Passed
Push — master ( 661d99...df5439 )
by Lars
08:03
created

UTF8::is_utf16()   C

Complexity

Conditions 16
Paths 97

Size

Total Lines 65
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 16

Importance

Changes 6
Bugs 5 Features 0
Metric Value
cc 16
eloc 39
c 6
b 5
f 0
nc 97
nop 2
dl 0
loc 65
ccs 38
cts 38
cp 1
crap 16
rs 5.5666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
final class UTF8
8
{
9
    /**
10
     * (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control])
11
     * This regular expression is a work around for http://bugs.exim.org/1279
12
     */
13
    const GRAPHEME_CLUSTER_RX = "(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])";
14
15
    /**
16
     * Bom => Byte-Length
17
     *
18
     * INFO: https://en.wikipedia.org/wiki/Byte_order_mark
19
     *
20
     * @var array
21
     */
22
    private static $BOM = [
23
        "\xef\xbb\xbf"     => 3, // UTF-8 BOM
24
        ''              => 6, // UTF-8 BOM as "WINDOWS-1252" (one char has [maybe] more then one byte ...)
25
        "\x00\x00\xfe\xff" => 4, // UTF-32 (BE) BOM
26
        '  þÿ'             => 6, // UTF-32 (BE) BOM as "WINDOWS-1252"
27
        "\xff\xfe\x00\x00" => 4, // UTF-32 (LE) BOM
28
        'ÿþ  '             => 6, // UTF-32 (LE) BOM as "WINDOWS-1252"
29
        "\xfe\xff"         => 2, // UTF-16 (BE) BOM
30
        'þÿ'               => 4, // UTF-16 (BE) BOM as "WINDOWS-1252"
31
        "\xff\xfe"         => 2, // UTF-16 (LE) BOM
32
        'ÿþ'               => 4, // UTF-16 (LE) BOM as "WINDOWS-1252"
33
    ];
34
35
    /**
36
     * Numeric code point => UTF-8 Character
37
     *
38
     * url: http://www.w3schools.com/charsets/ref_utf_punctuation.asp
39
     *
40
     * @var array
41
     */
42
    private static $WHITESPACE = [
43
        // NUL Byte
44
        0 => "\x0",
45
        // Tab
46
        9 => "\x9",
47
        // New Line
48
        10 => "\xa",
49
        // Vertical Tab
50
        11 => "\xb",
51
        // Carriage Return
52
        13 => "\xd",
53
        // Ordinary Space
54
        32 => "\x20",
55
        // NO-BREAK SPACE
56
        160 => "\xc2\xa0",
57
        // OGHAM SPACE MARK
58
        5760 => "\xe1\x9a\x80",
59
        // MONGOLIAN VOWEL SEPARATOR
60
        6158 => "\xe1\xa0\x8e",
61
        // EN QUAD
62
        8192 => "\xe2\x80\x80",
63
        // EM QUAD
64
        8193 => "\xe2\x80\x81",
65
        // EN SPACE
66
        8194 => "\xe2\x80\x82",
67
        // EM SPACE
68
        8195 => "\xe2\x80\x83",
69
        // THREE-PER-EM SPACE
70
        8196 => "\xe2\x80\x84",
71
        // FOUR-PER-EM SPACE
72
        8197 => "\xe2\x80\x85",
73
        // SIX-PER-EM SPACE
74
        8198 => "\xe2\x80\x86",
75
        // FIGURE SPACE
76
        8199 => "\xe2\x80\x87",
77
        // PUNCTUATION SPACE
78
        8200 => "\xe2\x80\x88",
79
        // THIN SPACE
80
        8201 => "\xe2\x80\x89",
81
        //HAIR SPACE
82
        8202 => "\xe2\x80\x8a",
83
        // LINE SEPARATOR
84
        8232 => "\xe2\x80\xa8",
85
        // PARAGRAPH SEPARATOR
86
        8233 => "\xe2\x80\xa9",
87
        // NARROW NO-BREAK SPACE
88
        8239 => "\xe2\x80\xaf",
89
        // MEDIUM MATHEMATICAL SPACE
90
        8287 => "\xe2\x81\x9f",
91
        // IDEOGRAPHIC SPACE
92
        12288 => "\xe3\x80\x80",
93
    ];
94
95
    /**
96
     * @var array
97
     */
98
    private static $WHITESPACE_TABLE = [
99
        'SPACE'                     => "\x20",
100
        'NO-BREAK SPACE'            => "\xc2\xa0",
101
        'OGHAM SPACE MARK'          => "\xe1\x9a\x80",
102
        'EN QUAD'                   => "\xe2\x80\x80",
103
        'EM QUAD'                   => "\xe2\x80\x81",
104
        'EN SPACE'                  => "\xe2\x80\x82",
105
        'EM SPACE'                  => "\xe2\x80\x83",
106
        'THREE-PER-EM SPACE'        => "\xe2\x80\x84",
107
        'FOUR-PER-EM SPACE'         => "\xe2\x80\x85",
108
        'SIX-PER-EM SPACE'          => "\xe2\x80\x86",
109
        'FIGURE SPACE'              => "\xe2\x80\x87",
110
        'PUNCTUATION SPACE'         => "\xe2\x80\x88",
111
        'THIN SPACE'                => "\xe2\x80\x89",
112
        'HAIR SPACE'                => "\xe2\x80\x8a",
113
        'LINE SEPARATOR'            => "\xe2\x80\xa8",
114
        'PARAGRAPH SEPARATOR'       => "\xe2\x80\xa9",
115
        'ZERO WIDTH SPACE'          => "\xe2\x80\x8b",
116
        'NARROW NO-BREAK SPACE'     => "\xe2\x80\xaf",
117
        'MEDIUM MATHEMATICAL SPACE' => "\xe2\x81\x9f",
118
        'IDEOGRAPHIC SPACE'         => "\xe3\x80\x80",
119
        'HALFWIDTH HANGUL FILLER'   => "\xef\xbe\xa0",
120
    ];
121
122
    /**
123
     * @var array{upper: string[], lower: string[]}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{upper at position 0 could not be parsed: Unknown type name 'array{upper' at position 0 in array{upper.
Loading history...
124
     */
125
    private static $COMMON_CASE_FOLD = [
126
        'upper' => [
127
            'µ',
128
            'ſ',
129
            "\xCD\x85",
130
            'ς',
131
            'ẞ',
132
            "\xCF\x90",
133
            "\xCF\x91",
134
            "\xCF\x95",
135
            "\xCF\x96",
136
            "\xCF\xB0",
137
            "\xCF\xB1",
138
            "\xCF\xB5",
139
            "\xE1\xBA\x9B",
140
            "\xE1\xBE\xBE",
141
        ],
142
        'lower' => [
143
            'μ',
144
            's',
145
            'ι',
146
            'σ',
147
            'ß',
148
            'β',
149
            'θ',
150
            'φ',
151
            'π',
152
            'κ',
153
            'ρ',
154
            'ε',
155
            "\xE1\xB9\xA1",
156
            'ι',
157
        ],
158
    ];
159
160
    /**
161
     * @var array
162
     */
163
    private static $SUPPORT = [];
164
165
    /**
166
     * @var array|null
167
     */
168
    private static $BROKEN_UTF8_FIX;
169
170
    /**
171
     * @var array|null
172
     */
173
    private static $WIN1252_TO_UTF8;
174
175
    /**
176
     * @var array|null
177
     */
178
    private static $INTL_TRANSLITERATOR_LIST;
179
180
    /**
181
     * @var array|null
182
     */
183
    private static $ENCODINGS;
184
185
    /**
186
     * @var array|null
187
     */
188
    private static $ORD;
189
190
    /**
191
     * @var array|null
192
     */
193
    private static $EMOJI;
194
195
    /**
196
     * @var array|null
197
     */
198
    private static $EMOJI_VALUES_CACHE;
199
200
    /**
201
     * @var array|null
202
     */
203
    private static $EMOJI_KEYS_CACHE;
204
205
    /**
206
     * @var array|null
207
     */
208
    private static $EMOJI_KEYS_REVERSIBLE_CACHE;
209
210
    /**
211
     * @var array|null
212
     */
213
    private static $CHR;
214
215
    /**
216
     * __construct()
217
     */
218 33
    public function __construct()
219
    {
220 33
    }
221
222
    /**
223
     * Return the character at the specified position: $str[1] like functionality.
224
     *
225
     * @param string $str      <p>A UTF-8 string.</p>
226
     * @param int    $pos      <p>The position of character to return.</p>
227
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
228
     *
229
     * @return string single multi-byte character
230
     */
231 3
    public static function access(string $str, int $pos, string $encoding = 'UTF-8'): string
232
    {
233 3
        if ($str === '' || $pos < 0) {
234 2
            return '';
235
        }
236
237 3
        if ($encoding === 'UTF-8') {
238 3
            return (string) \mb_substr($str, $pos, 1);
239
        }
240
241
        return (string) self::substr($str, $pos, 1, $encoding);
242
    }
243
244
    /**
245
     * Prepends UTF-8 BOM character to the string and returns the whole string.
246
     *
247
     * INFO: If BOM already existed there, the Input string is returned.
248
     *
249
     * @param string $str <p>The input string.</p>
250
     *
251
     * @return string the output string that contains BOM
252
     */
253 2
    public static function add_bom_to_string(string $str): string
254
    {
255 2
        if (self::string_has_bom($str) === false) {
256 2
            $str = self::bom() . $str;
257
        }
258
259 2
        return $str;
260
    }
261
262
    /**
263
     * Changes all keys in an array.
264
     *
265
     * @param array  $array    <p>The array to work on</p>
266
     * @param int    $case     [optional] <p> Either <strong>CASE_UPPER</strong><br>
267
     *                         or <strong>CASE_LOWER</strong> (default)</p>
268
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
269
     *
270
     * @return string[]
271
     *                  <p>An array with its keys lower- or uppercased.</p>
272
     */
273 2
    public static function array_change_key_case(
274
        array $array,
275
        int $case = \CASE_LOWER,
276
        string $encoding = 'UTF-8'
277
    ): array {
278
        if (
279 2
            $case !== \CASE_LOWER
280
            &&
281 2
            $case !== \CASE_UPPER
282
        ) {
283
            $case = \CASE_LOWER;
284
        }
285
286 2
        $return = [];
287 2
        foreach ($array as $key => &$value) {
288 2
            $key = $case === \CASE_LOWER
289 2
                ? self::strtolower((string) $key, $encoding)
290 2
                : self::strtoupper((string) $key, $encoding);
291
292 2
            $return[$key] = $value;
293
        }
294
295 2
        return $return;
296
    }
297
298
    /**
299
     * Returns the substring between $start and $end, if found, or an empty
300
     * string. An optional offset may be supplied from which to begin the
301
     * search for the start string.
302
     *
303
     * @param string $str
304
     * @param string $start    <p>Delimiter marking the start of the substring.</p>
305
     * @param string $end      <p>Delimiter marking the end of the substring.</p>
306
     * @param int    $offset   [optional] <p>Index from which to begin the search. Default: 0</p>
307
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
308
     *
309
     * @return string
310
     */
311 16
    public static function between(
312
        string $str,
313
        string $start,
314
        string $end,
315
        int $offset = 0,
316
        string $encoding = 'UTF-8'
317
    ): string {
318 16
        if ($encoding === 'UTF-8') {
319 8
            $start_position = \mb_strpos($str, $start, $offset);
320 8
            if ($start_position === false) {
321 1
                return '';
322
            }
323
324 7
            $substr_index = $start_position + (int) \mb_strlen($start);
325 7
            $end_position = \mb_strpos($str, $end, $substr_index);
326
            if (
327 7
                $end_position === false
328
                ||
329 7
                $end_position === $substr_index
330
            ) {
331 2
                return '';
332
            }
333
334 5
            return (string) \mb_substr($str, $substr_index, $end_position - $substr_index);
335
        }
336
337 8
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
338
339 8
        $start_position = self::strpos($str, $start, $offset, $encoding);
340 8
        if ($start_position === false) {
341 1
            return '';
342
        }
343
344 7
        $substr_index = $start_position + (int) self::strlen($start, $encoding);
345 7
        $end_position = self::strpos($str, $end, $substr_index, $encoding);
346
        if (
347 7
            $end_position === false
348
            ||
349 7
            $end_position === $substr_index
350
        ) {
351 2
            return '';
352
        }
353
354 5
        return (string) self::substr(
355 5
            $str,
356 5
            $substr_index,
357 5
            $end_position - $substr_index,
358 5
            $encoding
359
        );
360
    }
361
362
    /**
363
     * Convert binary into a string.
364
     *
365
     * @param mixed $bin 1|0
366
     *
367
     * @return string
368
     */
369 2
    public static function binary_to_str($bin): string
370
    {
371 2
        if (!isset($bin[0])) {
372
            return '';
373
        }
374
375 2
        $convert = \base_convert($bin, 2, 16);
376 2
        if ($convert === '0') {
377 1
            return '';
378
        }
379
380 2
        return \pack('H*', $convert);
381
    }
382
383
    /**
384
     * Returns the UTF-8 Byte Order Mark Character.
385
     *
386
     * INFO: take a look at UTF8::$bom for e.g. UTF-16 and UTF-32 BOM values
387
     *
388
     * @return string UTF-8 Byte Order Mark
389
     */
390 4
    public static function bom(): string
391
    {
392 4
        return "\xef\xbb\xbf";
393
    }
394
395
    /**
396
     * @alias of UTF8::chr_map()
397
     *
398
     * @param callable $callback
399
     * @param string   $str
400
     *
401
     * @return string[]
402
     *
403
     * @see UTF8::chr_map()
404
     */
405 2
    public static function callback($callback, string $str): array
406
    {
407 2
        return self::chr_map($callback, $str);
408
    }
409
410
    /**
411
     * Returns the character at $index, with indexes starting at 0.
412
     *
413
     * @param string $str      <p>The input string.</p>
414
     * @param int    $index    <p>Position of the character.</p>
415
     * @param string $encoding [optional] <p>Default is UTF-8</p>
416
     *
417
     * @return string the character at $index
418
     */
419 9
    public static function char_at(string $str, int $index, string $encoding = 'UTF-8'): string
420
    {
421 9
        if ($encoding === 'UTF-8') {
422 5
            return (string) \mb_substr($str, $index, 1);
423
        }
424
425 4
        return (string) self::substr($str, $index, 1, $encoding);
426
    }
427
428
    /**
429
     * Returns an array consisting of the characters in the string.
430
     *
431
     * @param string $str <p>The input string.</p>
432
     *
433
     * @return string[] an array of chars
434
     */
435 3
    public static function chars(string $str): array
436
    {
437 3
        return self::str_split($str);
438
    }
439
440
    /**
441
     * This method will auto-detect your server environment for UTF-8 support.
442
     *
443
     * @return true|null
444
     *
445
     * @internal <p>You don't need to run it manually, it will be triggered if it's needed.</p>
446
     */
447 5
    public static function checkForSupport()
448
    {
449 5
        if (!isset(self::$SUPPORT['already_checked_via_portable_utf8'])) {
450
            self::$SUPPORT['already_checked_via_portable_utf8'] = true;
451
452
            // http://php.net/manual/en/book.mbstring.php
453
            self::$SUPPORT['mbstring'] = self::mbstring_loaded();
454
            self::$SUPPORT['mbstring_func_overload'] = self::mbstring_overloaded();
455
            if (self::$SUPPORT['mbstring'] === true) {
456
                \mb_internal_encoding('UTF-8');
457
                /** @noinspection UnusedFunctionResultInspection */
458
                /** @noinspection PhpComposerExtensionStubsInspection */
459
                \mb_regex_encoding('UTF-8');
460
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
461
            }
462
463
            // http://php.net/manual/en/book.iconv.php
464
            self::$SUPPORT['iconv'] = self::iconv_loaded();
465
466
            // http://php.net/manual/en/book.intl.php
467
            self::$SUPPORT['intl'] = self::intl_loaded();
468
469
            // http://php.net/manual/en/class.intlchar.php
470
            self::$SUPPORT['intlChar'] = self::intlChar_loaded();
471
472
            // http://php.net/manual/en/book.ctype.php
473
            self::$SUPPORT['ctype'] = self::ctype_loaded();
474
475
            // http://php.net/manual/en/class.finfo.php
476
            self::$SUPPORT['finfo'] = self::finfo_loaded();
477
478
            // http://php.net/manual/en/book.json.php
479
            self::$SUPPORT['json'] = self::json_loaded();
480
481
            // http://php.net/manual/en/book.pcre.php
482
            self::$SUPPORT['pcre_utf8'] = self::pcre_utf8_support();
483
484
            self::$SUPPORT['symfony_polyfill_used'] = self::symfony_polyfill_used();
485
            if (self::$SUPPORT['symfony_polyfill_used'] === true) {
486
                \mb_internal_encoding('UTF-8');
487
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
488
            }
489
490
            return true;
491
        }
492
493 5
        return null;
494
    }
495
496
    /**
497
     * Generates a UTF-8 encoded character from the given code point.
498
     *
499
     * INFO: opposite to UTF8::ord()
500
     *
501
     * @param int|string $code_point <p>The code point for which to generate a character.</p>
502
     * @param string     $encoding   [optional] <p>Default is UTF-8</p>
503
     *
504
     * @return string|null multi-byte character, returns null on failure or empty input
505
     */
506 25
    public static function chr($code_point, string $encoding = 'UTF-8')
507
    {
508
        // init
509 25
        static $CHAR_CACHE = [];
510
511 25
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
512 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
513
        }
514
515
        if (
516 25
            $encoding !== 'UTF-8'
517
            &&
518 25
            $encoding !== 'ISO-8859-1'
519
            &&
520 25
            $encoding !== 'WINDOWS-1252'
521
            &&
522 25
            self::$SUPPORT['mbstring'] === false
523
        ) {
524
            \trigger_error('UTF8::chr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
525
        }
526
527 25
        $cache_key = $code_point . $encoding;
528 25
        if (isset($CHAR_CACHE[$cache_key]) === true) {
529 23
            return $CHAR_CACHE[$cache_key];
530
        }
531
532 14
        if ($code_point <= 127) { // use "simple"-char only until "\x80"
533
534 13
            if (self::$CHR === null) {
535
                self::$CHR = self::getData('chr');
536
            }
537
538
            /**
539
             * @psalm-suppress PossiblyNullArrayAccess
540
             */
541 13
            $chr = self::$CHR[$code_point];
542
543 13
            if ($encoding !== 'UTF-8') {
544 1
                $chr = self::encode($encoding, $chr);
545
            }
546
547 13
            return $CHAR_CACHE[$cache_key] = $chr;
548
        }
549
550
        //
551
        // fallback via "IntlChar"
552
        //
553
554 7
        if (self::$SUPPORT['intlChar'] === true) {
555
            /** @noinspection PhpComposerExtensionStubsInspection */
556 7
            $chr = \IntlChar::chr($code_point);
557
558 7
            if ($encoding !== 'UTF-8') {
559
                $chr = self::encode($encoding, $chr);
560
            }
561
562 7
            return $CHAR_CACHE[$cache_key] = $chr;
563
        }
564
565
        //
566
        // fallback via vanilla php
567
        //
568
569
        if (self::$CHR === null) {
570
            self::$CHR = self::getData('chr');
571
        }
572
573
        $code_point = (int) $code_point;
574
        if ($code_point <= 0x7F) {
575
            /**
576
             * @psalm-suppress PossiblyNullArrayAccess
577
             */
578
            $chr = self::$CHR[$code_point];
579
        } elseif ($code_point <= 0x7FF) {
580
            /**
581
             * @psalm-suppress PossiblyNullArrayAccess
582
             */
583
            $chr = self::$CHR[($code_point >> 6) + 0xC0] .
584
                   self::$CHR[($code_point & 0x3F) + 0x80];
585
        } elseif ($code_point <= 0xFFFF) {
586
            /**
587
             * @psalm-suppress PossiblyNullArrayAccess
588
             */
589
            $chr = self::$CHR[($code_point >> 12) + 0xE0] .
590
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
591
                   self::$CHR[($code_point & 0x3F) + 0x80];
592
        } else {
593
            /**
594
             * @psalm-suppress PossiblyNullArrayAccess
595
             */
596
            $chr = self::$CHR[($code_point >> 18) + 0xF0] .
597
                   self::$CHR[(($code_point >> 12) & 0x3F) + 0x80] .
598
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
599
                   self::$CHR[($code_point & 0x3F) + 0x80];
600
        }
601
602
        if ($encoding !== 'UTF-8') {
603
            $chr = self::encode($encoding, $chr);
604
        }
605
606
        return $CHAR_CACHE[$cache_key] = $chr;
607
    }
608
609
    /**
610
     * Applies callback to all characters of a string.
611
     *
612
     * @param callable $callback <p>The callback function.</p>
613
     * @param string   $str      <p>UTF-8 string to run callback on.</p>
614
     *
615
     * @return string[]
616
     *                   <p>The outcome of the callback, as array.</p>
617
     */
618 2
    public static function chr_map($callback, string $str): array
619
    {
620 2
        return \array_map(
621 2
            $callback,
622 2
            self::str_split($str)
623
        );
624
    }
625
626
    /**
627
     * Generates an array of byte length of each character of a Unicode string.
628
     *
629
     * 1 byte => U+0000  - U+007F
630
     * 2 byte => U+0080  - U+07FF
631
     * 3 byte => U+0800  - U+FFFF
632
     * 4 byte => U+10000 - U+10FFFF
633
     *
634
     * @param string $str <p>The original unicode string.</p>
635
     *
636
     * @return int[] an array of byte lengths of each character
637
     */
638 4
    public static function chr_size_list(string $str): array
639
    {
640 4
        if ($str === '') {
641 4
            return [];
642
        }
643
644 4
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
645
            return \array_map(
646
                static function (string $data): int {
647
                    // "mb_" is available if overload is used, so use it ...
648
                    return \mb_strlen($data, 'CP850'); // 8-BIT
649
                },
650
                self::str_split($str)
651
            );
652
        }
653
654 4
        return \array_map('\strlen', self::str_split($str));
655
    }
656
657
    /**
658
     * Get a decimal code representation of a specific character.
659
     *
660
     * @param string $char <p>The input character.</p>
661
     *
662
     * @return int
663
     */
664 4
    public static function chr_to_decimal(string $char): int
665
    {
666 4
        $code = self::ord($char[0]);
667 4
        $bytes = 1;
668
669 4
        if (!($code & 0x80)) {
670
            // 0xxxxxxx
671 4
            return $code;
672
        }
673
674 4
        if (($code & 0xe0) === 0xc0) {
675
            // 110xxxxx
676 4
            $bytes = 2;
677 4
            $code &= ~0xc0;
678 4
        } elseif (($code & 0xf0) === 0xe0) {
679
            // 1110xxxx
680 4
            $bytes = 3;
681 4
            $code &= ~0xe0;
682 2
        } elseif (($code & 0xf8) === 0xf0) {
683
            // 11110xxx
684 2
            $bytes = 4;
685 2
            $code &= ~0xf0;
686
        }
687
688 4
        for ($i = 2; $i <= $bytes; ++$i) {
689
            // 10xxxxxx
690 4
            $code = ($code << 6) + (self::ord($char[$i - 1]) & ~0x80);
691
        }
692
693 4
        return $code;
694
    }
695
696
    /**
697
     * Get hexadecimal code point (U+xxxx) of a UTF-8 encoded character.
698
     *
699
     * @param int|string $char   <p>The input character</p>
700
     * @param string     $prefix [optional]
701
     *
702
     * @return string The code point encoded as U+xxxx
703
     */
704 2
    public static function chr_to_hex($char, string $prefix = 'U+'): string
705
    {
706 2
        if ($char === '') {
707 2
            return '';
708
        }
709
710 2
        if ($char === '&#0;') {
711 2
            $char = '';
712
        }
713
714 2
        return self::int_to_hex(self::ord((string) $char), $prefix);
715
    }
716
717
    /**
718
     * alias for "UTF8::chr_to_decimal()"
719
     *
720
     * @param string $chr
721
     *
722
     * @return int
723
     *
724
     * @see UTF8::chr_to_decimal()
725
     * @deprecated <p>please use "UTF8::chr_to_decimal()"</p>
726
     */
727 2
    public static function chr_to_int(string $chr): int
728
    {
729 2
        return self::chr_to_decimal($chr);
730
    }
731
732
    /**
733
     * Splits a string into smaller chunks and multiple lines, using the specified line ending character.
734
     *
735
     * @param string $body         <p>The original string to be split.</p>
736
     * @param int    $chunk_length [optional] <p>The maximum character length of a chunk.</p>
737
     * @param string $end          [optional] <p>The character(s) to be inserted at the end of each chunk.</p>
738
     *
739
     * @return string the chunked string
740
     */
741 4
    public static function chunk_split(string $body, int $chunk_length = 76, string $end = "\r\n"): string
742
    {
743 4
        return \implode($end, self::str_split($body, $chunk_length));
744
    }
745
746
    /**
747
     * Accepts a string and removes all non-UTF-8 characters from it + extras if needed.
748
     *
749
     * @param string $str                           <p>The string to be sanitized.</p>
750
     * @param bool   $remove_bom                    [optional] <p>Set to true, if you need to remove UTF-BOM.</p>
751
     * @param bool   $normalize_whitespace          [optional] <p>Set to true, if you need to normalize the
752
     *                                              whitespace.</p>
753
     * @param bool   $normalize_msword              [optional] <p>Set to true, if you need to normalize MS Word chars
754
     *                                              e.g.: "…"
755
     *                                              => "..."</p>
756
     * @param bool   $keep_non_breaking_space       [optional] <p>Set to true, to keep non-breaking-spaces, in
757
     *                                              combination with
758
     *                                              $normalize_whitespace</p>
759
     * @param bool   $replace_diamond_question_mark [optional] <p>Set to true, if you need to remove diamond question
760
     *                                              mark e.g.: "�"</p>
761
     * @param bool   $remove_invisible_characters   [optional] <p>Set to false, if you not want to remove invisible
762
     *                                              characters e.g.: "\0"</p>
763
     *
764
     * @return string clean UTF-8 encoded string
765
     */
766 87
    public static function clean(
767
        string $str,
768
        bool $remove_bom = false,
769
        bool $normalize_whitespace = false,
770
        bool $normalize_msword = false,
771
        bool $keep_non_breaking_space = false,
772
        bool $replace_diamond_question_mark = false,
773
        bool $remove_invisible_characters = true
774
    ): string {
775
        // http://stackoverflow.com/questions/1401317/remove-non-utf8-characters-from-string
776
        // caused connection reset problem on larger strings
777
778 87
        $regex = '/
779
          (
780
            (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
781
            |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
782
            |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
783
            |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3
784
            ){1,100}                      # ...one or more times
785
          )
786
        | ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
787
        | ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
788
        /x';
789
        /** @noinspection NotOptimalRegularExpressionsInspection */
790 87
        $str = (string) \preg_replace($regex, '$1', $str);
791
792 87
        if ($replace_diamond_question_mark === true) {
793 33
            $str = self::replace_diamond_question_mark($str, '');
794
        }
795
796 87
        if ($remove_invisible_characters === true) {
797 87
            $str = self::remove_invisible_characters($str);
798
        }
799
800 87
        if ($normalize_whitespace === true) {
801 37
            $str = self::normalize_whitespace($str, $keep_non_breaking_space);
802
        }
803
804 87
        if ($normalize_msword === true) {
805 4
            $str = self::normalize_msword($str);
806
        }
807
808 87
        if ($remove_bom === true) {
809 37
            $str = self::remove_bom($str);
810
        }
811
812 87
        return $str;
813
    }
814
815
    /**
816
     * Clean-up a string and show only printable UTF-8 chars at the end  + fix UTF-8 encoding.
817
     *
818
     * @param string $str <p>The input string.</p>
819
     *
820
     * @return string
821
     */
822 33
    public static function cleanup($str): string
823
    {
824
        // init
825 33
        $str = (string) $str;
826
827 33
        if ($str === '') {
828 5
            return '';
829
        }
830
831
        // fixed ISO <-> UTF-8 Errors
832 33
        $str = self::fix_simple_utf8($str);
833
834
        // remove all none UTF-8 symbols
835
        // && remove diamond question mark (�)
836
        // && remove remove invisible characters (e.g. "\0")
837
        // && remove BOM
838
        // && normalize whitespace chars (but keep non-breaking-spaces)
839 33
        return self::clean(
840 33
            $str,
841 33
            true,
842 33
            true,
843 33
            false,
844 33
            true,
845 33
            true,
846 33
            true
847
        );
848
    }
849
850
    /**
851
     * Accepts a string or a array of strings and returns an array of Unicode code points.
852
     *
853
     * INFO: opposite to UTF8::string()
854
     *
855
     * @param string|string[] $arg     <p>A UTF-8 encoded string or an array of such strings.</p>
856
     * @param bool            $u_style <p>If True, will return code points in U+xxxx format,
857
     *                                 default, code points will be returned as integers.</p>
858
     *
859
     * @return array<int|string>
860
     *                           The array of code points:<br>
861
     *                           array<int> for $u_style === false<br>
862
     *                           array<string> for $u_style === true<br>
863
     */
864 12
    public static function codepoints($arg, bool $u_style = false): array
865
    {
866 12
        if (\is_string($arg) === true) {
867 12
            $arg = self::str_split($arg);
868
        }
869
870 12
        $arg = \array_map(
871
            [
872 12
                self::class,
873
                'ord',
874
            ],
875 12
            $arg
876
        );
877
878 12
        if (\count($arg) === 0) {
879 7
            return [];
880
        }
881
882 11
        if ($u_style === true) {
883 2
            $arg = \array_map(
884
                [
885 2
                    self::class,
886
                    'int_to_hex',
887
                ],
888 2
                $arg
889
            );
890
        }
891
892 11
        return $arg;
893
    }
894
895
    /**
896
     * Trims the string and replaces consecutive whitespace characters with a
897
     * single space. This includes tabs and newline characters, as well as
898
     * multibyte whitespace such as the thin space and ideographic space.
899
     *
900
     * @param string $str <p>The input string.</p>
901
     *
902
     * @return string string with a trimmed $str and condensed whitespace
903
     */
904 13
    public static function collapse_whitespace(string $str): string
905
    {
906 13
        if (self::$SUPPORT['mbstring'] === true) {
907
            /** @noinspection PhpComposerExtensionStubsInspection */
908 13
            return \trim((string) \mb_ereg_replace('[[:space:]]+', ' ', $str));
909
        }
910
911
        return \trim(self::regex_replace($str, '[[:space:]]+', ' '));
912
    }
913
914
    /**
915
     * Returns count of characters used in a string.
916
     *
917
     * @param string $str                     <p>The input string.</p>
918
     * @param bool   $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
919
     * @param bool   $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
920
     *
921
     * @return int[] an associative array of Character as keys and
922
     *               their count as values
923
     */
924 19
    public static function count_chars(
925
        string $str,
926
        bool $clean_utf8 = false,
927
        bool $try_to_use_mb_functions = true
928
    ): array {
929 19
        return \array_count_values(
930 19
            self::str_split(
931 19
                $str,
932 19
                1,
933 19
                $clean_utf8,
934 19
                $try_to_use_mb_functions
935
            )
936
        );
937
    }
938
939
    /**
940
     * Remove css media-queries.
941
     *
942
     * @param string $str
943
     *
944
     * @return string
945
     */
946 1
    public static function css_stripe_media_queries(string $str): string
947
    {
948 1
        return (string) \preg_replace(
949 1
            '#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#isumU',
950 1
            '',
951 1
            $str
952
        );
953
    }
954
955
    /**
956
     * Checks whether ctype is available on the server.
957
     *
958
     * @return bool
959
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
960
     */
961
    public static function ctype_loaded(): bool
962
    {
963
        return \extension_loaded('ctype');
964
    }
965
966
    /**
967
     * Converts an int value into a UTF-8 character.
968
     *
969
     * @param mixed $int
970
     *
971
     * @return string
972
     */
973 19
    public static function decimal_to_chr($int): string
974
    {
975 19
        return self::html_entity_decode('&#' . $int . ';', \ENT_QUOTES | \ENT_HTML5);
976
    }
977
978
    /**
979
     * Decodes a MIME header field
980
     *
981
     * @param string $str
982
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
983
     *
984
     * @return false|string
985
     *                      A decoded MIME field on success,
986
     *                      or false if an error occurs during the decoding
987
     */
988
    public static function decode_mimeheader($str, string $encoding = 'UTF-8')
989
    {
990
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
991
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
992
        }
993
994
        if (self::$SUPPORT['iconv'] === true) {
995
            return \iconv_mime_decode($str, \ICONV_MIME_DECODE_CONTINUE_ON_ERROR, $encoding);
996
        }
997
998
        if ($encoding !== 'UTF-8') {
999
            $str = self::encode($encoding, $str);
1000
        }
1001
1002
        return \mb_decode_mimeheader($str);
1003
    }
1004
1005
    /**
1006
     * Decodes a string which was encoded by "UTF8::emoji_encode()".
1007
     *
1008
     * @param string $str                            <p>The input string.</p>
1009
     * @param bool   $use_reversible_string_mappings [optional] <p>
1010
     *                                               When <b>TRUE</b>, we se a reversible string mapping
1011
     *                                               between "emoji_encode" and "emoji_decode".</p>
1012
     *
1013
     * @return string
1014
     */
1015 9
    public static function emoji_decode(
1016
        string $str,
1017
        bool $use_reversible_string_mappings = false
1018
    ): string {
1019 9
        self::initEmojiData();
1020
1021 9
        if ($use_reversible_string_mappings === true) {
1022 9
            return (string) \str_replace(
1023 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1024 9
                (array) self::$EMOJI_VALUES_CACHE,
1025 9
                $str
1026
            );
1027
        }
1028
1029 1
        return (string) \str_replace(
1030 1
            (array) self::$EMOJI_KEYS_CACHE,
1031 1
            (array) self::$EMOJI_VALUES_CACHE,
1032 1
            $str
1033
        );
1034
    }
1035
1036
    /**
1037
     * Encode a string with emoji chars into a non-emoji string.
1038
     *
1039
     * @param string $str                            <p>The input string</p>
1040
     * @param bool   $use_reversible_string_mappings [optional] <p>
1041
     *                                               when <b>TRUE</b>, we se a reversible string mapping
1042
     *                                               between "emoji_encode" and "emoji_decode"</p>
1043
     *
1044
     * @return string
1045
     */
1046 12
    public static function emoji_encode(
1047
        string $str,
1048
        bool $use_reversible_string_mappings = false
1049
    ): string {
1050 12
        self::initEmojiData();
1051
1052 12
        if ($use_reversible_string_mappings === true) {
1053 9
            return (string) \str_replace(
1054 9
                (array) self::$EMOJI_VALUES_CACHE,
1055 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1056 9
                $str
1057
            );
1058
        }
1059
1060 4
        return (string) \str_replace(
1061 4
            (array) self::$EMOJI_VALUES_CACHE,
1062 4
            (array) self::$EMOJI_KEYS_CACHE,
1063 4
            $str
1064
        );
1065
    }
1066
1067
    /**
1068
     * Encode a string with a new charset-encoding.
1069
     *
1070
     * INFO:  This function will also try to fix broken / double encoding,
1071
     *        so you can call this function also on a UTF-8 string and you don't mess up the string.
1072
     *
1073
     * @param string $to_encoding                   <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.</p>
1074
     * @param string $str                           <p>The input string</p>
1075
     * @param bool   $auto_detect_the_from_encoding [optional] <p>Force the new encoding (we try to fix broken / double
1076
     *                                              encoding for UTF-8)<br> otherwise we auto-detect the current
1077
     *                                              string-encoding</p>
1078
     * @param string $from_encoding                 [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1079
     *                                              A empty string will trigger the autodetect anyway.</p>
1080
     *
1081
     * @return string
1082
     *
1083
     * @psalm-suppress InvalidReturnStatement
1084
     */
1085 28
    public static function encode(
1086
        string $to_encoding,
1087
        string $str,
1088
        bool $auto_detect_the_from_encoding = true,
1089
        string $from_encoding = ''
1090
    ): string {
1091 28
        if ($str === '' || $to_encoding === '') {
1092 13
            return $str;
1093
        }
1094
1095 28
        if ($to_encoding !== 'UTF-8' && $to_encoding !== 'CP850') {
1096 7
            $to_encoding = self::normalize_encoding($to_encoding, 'UTF-8');
1097
        }
1098
1099 28
        if ($from_encoding && $from_encoding !== 'UTF-8' && $from_encoding !== 'CP850') {
1100 2
            $from_encoding = self::normalize_encoding($from_encoding, null);
1101
        }
1102
1103
        if (
1104 28
            $to_encoding
1105
            &&
1106 28
            $from_encoding
1107
            &&
1108 28
            $from_encoding === $to_encoding
1109
        ) {
1110
            return $str;
1111
        }
1112
1113 28
        if ($to_encoding === 'JSON') {
1114 1
            $return = self::json_encode($str);
1115 1
            if ($return === false) {
1116
                throw new \InvalidArgumentException('The input string [' . $str . '] can not be used for json_encode().');
1117
            }
1118
1119 1
            return $return;
1120
        }
1121 28
        if ($from_encoding === 'JSON') {
1122 1
            $str = self::json_decode($str);
1123 1
            $from_encoding = '';
1124
        }
1125
1126 28
        if ($to_encoding === 'BASE64') {
1127 2
            return \base64_encode($str);
1128
        }
1129 28
        if ($from_encoding === 'BASE64') {
1130 2
            $str = \base64_decode($str, true);
1131 2
            $from_encoding = '';
1132
        }
1133
1134 28
        if ($to_encoding === 'HTML-ENTITIES') {
1135 2
            return self::html_encode($str, true, 'UTF-8');
1136
        }
1137 28
        if ($from_encoding === 'HTML-ENTITIES') {
1138 2
            $str = self::html_entity_decode($str, \ENT_COMPAT, 'UTF-8');
1139 2
            $from_encoding = '';
1140
        }
1141
1142 28
        $from_encoding_auto_detected = false;
1143
        if (
1144 28
            $auto_detect_the_from_encoding === true
1145
            ||
1146 28
            !$from_encoding
1147
        ) {
1148 28
            $from_encoding_auto_detected = self::str_detect_encoding($str);
1149
        }
1150
1151
        // DEBUG
1152
        //var_dump($to_encoding, $from_encoding, $from_encoding_auto_detected, $str, "\n\n");
1153
1154 28
        if ($from_encoding_auto_detected !== false) {
1155
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1156 24
            $from_encoding = $from_encoding_auto_detected;
1157 7
        } elseif ($auto_detect_the_from_encoding === true) {
1158
            // fallback for the "autodetect"-mode
1159 7
            return self::to_utf8($str);
1160
        }
1161
1162
        if (
1163 24
            !$from_encoding
1164
            ||
1165 24
            $from_encoding === $to_encoding
1166
        ) {
1167 15
            return $str;
1168
        }
1169
1170
        if (
1171 19
            $to_encoding === 'UTF-8'
1172
            &&
1173
            (
1174 17
                $from_encoding === 'WINDOWS-1252'
1175
                ||
1176 19
                $from_encoding === 'ISO-8859-1'
1177
            )
1178
        ) {
1179 13
            return self::to_utf8($str);
1180
        }
1181
1182
        if (
1183 12
            $to_encoding === 'ISO-8859-1'
1184
            &&
1185
            (
1186 6
                $from_encoding === 'WINDOWS-1252'
1187
                ||
1188 12
                $from_encoding === 'UTF-8'
1189
            )
1190
        ) {
1191 6
            return self::to_iso8859($str);
1192
        }
1193
1194
        if (
1195 10
            $to_encoding !== 'UTF-8'
1196
            &&
1197 10
            $to_encoding !== 'ISO-8859-1'
1198
            &&
1199 10
            $to_encoding !== 'WINDOWS-1252'
1200
            &&
1201 10
            self::$SUPPORT['mbstring'] === false
1202
        ) {
1203
            \trigger_error('UTF8::encode() without mbstring cannot handle "' . $to_encoding . '" encoding', \E_USER_WARNING);
1204
        }
1205
1206 10
        if (self::$SUPPORT['mbstring'] === true) {
1207
            // warning: do not use the symfony polyfill here
1208 10
            $str_encoded = \mb_convert_encoding(
1209 10
                $str,
1210 10
                $to_encoding,
1211 10
                $from_encoding
1212
            );
1213
1214 10
            if ($str_encoded) {
1215 10
                return $str_encoded;
1216
            }
1217
        }
1218
1219
        $return = \iconv($from_encoding, $to_encoding, $str);
1220
        if ($return !== false) {
1221
            return $return;
1222
        }
1223
1224
        return $str;
1225
    }
1226
1227
    /**
1228
     * @param string $str
1229
     * @param string $from_charset      [optional] <p>Set the input charset.</p>
1230
     * @param string $to_charset        [optional] <p>Set the output charset.</p>
1231
     * @param string $transfer_encoding [optional] <p>Set the transfer encoding.</p>
1232
     * @param string $linefeed          [optional] <p>Set the used linefeed.</p>
1233
     * @param int    $indent            [optional] <p>Set the max length indent.</p>
1234
     *
1235
     * @return false|string
1236
     *                      <p>An encoded MIME field on success,
1237
     *                      or false if an error occurs during the encoding.</p>
1238
     */
1239
    public static function encode_mimeheader(
1240
        $str,
1241
        $from_charset = 'UTF-8',
1242
        $to_charset = 'UTF-8',
1243
        $transfer_encoding = 'Q',
1244
        $linefeed = '\\r\\n',
1245
        $indent = 76
1246
    ) {
1247
        if ($from_charset !== 'UTF-8' && $from_charset !== 'CP850') {
1248
            $from_charset = self::normalize_encoding($from_charset, 'UTF-8');
1249
        }
1250
1251
        if ($to_charset !== 'UTF-8' && $to_charset !== 'CP850') {
1252
            $to_charset = self::normalize_encoding($to_charset, 'UTF-8');
1253
        }
1254
1255
        return \iconv_mime_encode(
1256
            '',
1257
            $str,
1258
            [
1259
                'scheme'           => $transfer_encoding,
1260
                'line-length'      => $indent,
1261
                'input-charset'    => $from_charset,
1262
                'output-charset'   => $to_charset,
1263
                'line-break-chars' => $linefeed,
1264
            ]
1265
        );
1266
    }
1267
1268
    /**
1269
     * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
1270
     *
1271
     * @param string   $str                       <p>The input string.</p>
1272
     * @param string   $search                    <p>The searched string.</p>
1273
     * @param int|null $length                    [optional] <p>Default: null === text->length / 2</p>
1274
     * @param string   $replacer_for_skipped_text [optional] <p>Default: …</p>
1275
     * @param string   $encoding                  [optional] <p>Set the charset for e.g. "mb_" function</p>
1276
     *
1277
     * @return string
1278
     */
1279 1
    public static function extract_text(
1280
        string $str,
1281
        string $search = '',
1282
        int $length = null,
1283
        string $replacer_for_skipped_text = '…',
1284
        string $encoding = 'UTF-8'
1285
    ): string {
1286 1
        if ($str === '') {
1287 1
            return '';
1288
        }
1289
1290 1
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1291
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1292
        }
1293
1294 1
        $trim_chars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1295
1296 1
        if ($length === null) {
1297 1
            $length = (int) \round((int) self::strlen($str, $encoding) / 2, 0);
1298
        }
1299
1300 1
        if ($search === '') {
1301 1
            if ($encoding === 'UTF-8') {
1302 1
                if ($length > 0) {
1303 1
                    $string_length = (int) \mb_strlen($str);
1304 1
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1305
                } else {
1306 1
                    $end = 0;
1307
                }
1308
1309 1
                $pos = (int) \min(
1310 1
                    \mb_strpos($str, ' ', $end),
1311 1
                    \mb_strpos($str, '.', $end)
1312
                );
1313
            } else {
1314
                if ($length > 0) {
1315
                    $string_length = (int) self::strlen($str, $encoding);
1316
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1317
                } else {
1318
                    $end = 0;
1319
                }
1320
1321
                $pos = (int) \min(
1322
                    self::strpos($str, ' ', $end, $encoding),
1323
                    self::strpos($str, '.', $end, $encoding)
1324
                );
1325
            }
1326
1327 1
            if ($pos) {
1328 1
                if ($encoding === 'UTF-8') {
1329 1
                    $str_sub = \mb_substr($str, 0, $pos);
1330
                } else {
1331
                    $str_sub = self::substr($str, 0, $pos, $encoding);
1332
                }
1333
1334 1
                if ($str_sub === false) {
1335
                    return '';
1336
                }
1337
1338 1
                return \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1339
            }
1340
1341
            return $str;
1342
        }
1343
1344 1
        if ($encoding === 'UTF-8') {
1345 1
            $word_position = (int) \mb_stripos($str, $search);
1346 1
            $half_side = (int) ($word_position - $length / 2 + (int) \mb_strlen($search) / 2);
1347
        } else {
1348
            $word_position = (int) self::stripos($str, $search, 0, $encoding);
1349
            $half_side = (int) ($word_position - $length / 2 + (int) self::strlen($search, $encoding) / 2);
1350
        }
1351
1352 1
        $pos_start = 0;
1353 1
        if ($half_side > 0) {
1354 1
            if ($encoding === 'UTF-8') {
1355 1
                $half_text = \mb_substr($str, 0, $half_side);
1356
            } else {
1357
                $half_text = self::substr($str, 0, $half_side, $encoding);
1358
            }
1359 1
            if ($half_text !== false) {
1360 1
                if ($encoding === 'UTF-8') {
1361 1
                    $pos_start = (int) \max(
1362 1
                        \mb_strrpos($half_text, ' '),
1363 1
                        \mb_strrpos($half_text, '.')
1364
                    );
1365
                } else {
1366
                    $pos_start = (int) \max(
1367
                        self::strrpos($half_text, ' ', 0, $encoding),
1368
                        self::strrpos($half_text, '.', 0, $encoding)
1369
                    );
1370
                }
1371
            }
1372
        }
1373
1374 1
        if ($word_position && $half_side > 0) {
1375 1
            $offset = $pos_start + $length - 1;
1376 1
            $real_length = (int) self::strlen($str, $encoding);
1377
1378 1
            if ($offset > $real_length) {
1379
                $offset = $real_length;
1380
            }
1381
1382 1
            if ($encoding === 'UTF-8') {
1383 1
                $pos_end = (int) \min(
1384 1
                    \mb_strpos($str, ' ', $offset),
1385 1
                    \mb_strpos($str, '.', $offset)
1386 1
                ) - $pos_start;
1387
            } else {
1388
                $pos_end = (int) \min(
1389
                    self::strpos($str, ' ', $offset, $encoding),
1390
                    self::strpos($str, '.', $offset, $encoding)
1391
                ) - $pos_start;
1392
            }
1393
1394 1
            if (!$pos_end || $pos_end <= 0) {
1395 1
                if ($encoding === 'UTF-8') {
1396 1
                    $str_sub = \mb_substr($str, $pos_start, (int) \mb_strlen($str));
1397
                } else {
1398
                    $str_sub = self::substr($str, $pos_start, (int) self::strlen($str, $encoding), $encoding);
1399
                }
1400 1
                if ($str_sub !== false) {
1401 1
                    $extract = $replacer_for_skipped_text . \ltrim($str_sub, $trim_chars);
1402
                } else {
1403 1
                    $extract = '';
1404
                }
1405
            } else {
1406 1
                if ($encoding === 'UTF-8') {
1407 1
                    $str_sub = \mb_substr($str, $pos_start, $pos_end);
1408
                } else {
1409
                    $str_sub = self::substr($str, $pos_start, $pos_end, $encoding);
1410
                }
1411 1
                if ($str_sub !== false) {
1412 1
                    $extract = $replacer_for_skipped_text . \trim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1413
                } else {
1414 1
                    $extract = '';
1415
                }
1416
            }
1417
        } else {
1418 1
            $offset = $length - 1;
1419 1
            $true_length = (int) self::strlen($str, $encoding);
1420
1421 1
            if ($offset > $true_length) {
1422
                $offset = $true_length;
1423
            }
1424
1425 1
            if ($encoding === 'UTF-8') {
1426 1
                $pos_end = (int) \min(
1427 1
                    \mb_strpos($str, ' ', $offset),
1428 1
                    \mb_strpos($str, '.', $offset)
1429
                );
1430
            } else {
1431
                $pos_end = (int) \min(
1432
                    self::strpos($str, ' ', $offset, $encoding),
1433
                    self::strpos($str, '.', $offset, $encoding)
1434
                );
1435
            }
1436
1437 1
            if ($pos_end) {
1438 1
                if ($encoding === 'UTF-8') {
1439 1
                    $str_sub = \mb_substr($str, 0, $pos_end);
1440
                } else {
1441
                    $str_sub = self::substr($str, 0, $pos_end, $encoding);
1442
                }
1443 1
                if ($str_sub !== false) {
1444 1
                    $extract = \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1445
                } else {
1446 1
                    $extract = '';
1447
                }
1448
            } else {
1449 1
                $extract = $str;
1450
            }
1451
        }
1452
1453 1
        return $extract;
1454
    }
1455
1456
    /**
1457
     * Reads entire file into a string.
1458
     *
1459
     * WARNING: Do not use UTF-8 Option ($convert_to_utf8) for binary files (e.g.: images) !!!
1460
     *
1461
     * @see http://php.net/manual/en/function.file-get-contents.php
1462
     *
1463
     * @param string        $filename         <p>
1464
     *                                        Name of the file to read.
1465
     *                                        </p>
1466
     * @param bool          $use_include_path [optional] <p>
1467
     *                                        Prior to PHP 5, this parameter is called
1468
     *                                        use_include_path and is a bool.
1469
     *                                        As of PHP 5 the FILE_USE_INCLUDE_PATH can be used
1470
     *                                        to trigger include path
1471
     *                                        search.
1472
     *                                        </p>
1473
     * @param resource|null $context          [optional] <p>
1474
     *                                        A valid context resource created with
1475
     *                                        stream_context_create. If you don't need to use a
1476
     *                                        custom context, you can skip this parameter by &null;.
1477
     *                                        </p>
1478
     * @param int|null      $offset           [optional] <p>
1479
     *                                        The offset where the reading starts.
1480
     *                                        </p>
1481
     * @param int|null      $max_length       [optional] <p>
1482
     *                                        Maximum length of data read. The default is to read until end
1483
     *                                        of file is reached.
1484
     *                                        </p>
1485
     * @param int           $timeout          <p>The time in seconds for the timeout.</p>
1486
     * @param bool          $convert_to_utf8  <strong>WARNING!!!</strong> <p>Maybe you can't use this option for
1487
     *                                        some files, because they used non default utf-8 chars. Binary files
1488
     *                                        like images or pdf will not be converted.</p>
1489
     * @param string        $from_encoding    [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1490
     *                                        A empty string will trigger the autodetect anyway.</p>
1491
     *
1492
     * @return false|string
1493
     *                      <p>The function returns the read data as string or <b>false</b> on failure.</p>
1494
     */
1495 12
    public static function file_get_contents(
1496
        string $filename,
1497
        bool $use_include_path = false,
1498
        $context = null,
1499
        int $offset = null,
1500
        int $max_length = null,
1501
        int $timeout = 10,
1502
        bool $convert_to_utf8 = true,
1503
        string $from_encoding = ''
1504
    ) {
1505
        // init
1506 12
        $filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
1507
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1508 12
        if ($filename === false) {
1509
            return false;
1510
        }
1511
1512 12
        if ($timeout && $context === null) {
1513 9
            $context = \stream_context_create(
1514
                [
1515
                    'http' => [
1516 9
                        'timeout' => $timeout,
1517
                    ],
1518
                ]
1519
            );
1520
        }
1521
1522 12
        if ($offset === null) {
1523 12
            $offset = 0;
1524
        }
1525
1526 12
        if (\is_int($max_length) === true) {
1527 2
            $data = \file_get_contents($filename, $use_include_path, $context, $offset, $max_length);
1528
        } else {
1529 12
            $data = \file_get_contents($filename, $use_include_path, $context, $offset);
1530
        }
1531
1532
        // return false on error
1533 12
        if ($data === false) {
1534
            return false;
1535
        }
1536
1537 12
        if ($convert_to_utf8 === true) {
1538
            if (
1539 12
                self::is_binary($data, true) !== true
1540
                ||
1541 9
                self::is_utf16($data, false) !== false
1542
                ||
1543 12
                self::is_utf32($data, false) !== false
1544
            ) {
1545 9
                $data = self::encode('UTF-8', $data, false, $from_encoding);
1546 9
                $data = self::cleanup($data);
1547
            }
1548
        }
1549
1550 12
        return $data;
1551
    }
1552
1553
    /**
1554
     * Checks if a file starts with BOM (Byte Order Mark) character.
1555
     *
1556
     * @param string $file_path <p>Path to a valid file.</p>
1557
     *
1558
     * @throws \RuntimeException if file_get_contents() returned false
1559
     *
1560
     * @return bool
1561
     *              <p><strong>true</strong> if the file has BOM at the start, <strong>false</strong> otherwise</p>
1562
     */
1563 2
    public static function file_has_bom(string $file_path): bool
1564
    {
1565 2
        $file_content = \file_get_contents($file_path);
1566 2
        if ($file_content === false) {
1567
            throw new \RuntimeException('file_get_contents() returned false for:' . $file_path);
1568
        }
1569
1570 2
        return self::string_has_bom($file_content);
1571
    }
1572
1573
    /**
1574
     * Normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1575
     *
1576
     * @param mixed  $var
1577
     * @param int    $normalization_form
1578
     * @param string $leading_combining
1579
     *
1580
     * @return mixed
1581
     */
1582 62
    public static function filter(
1583
        $var,
1584
        int $normalization_form = \Normalizer::NFC,
1585
        string $leading_combining = '◌'
1586
    ) {
1587 62
        switch (\gettype($var)) {
1588 62
            case 'array':
1589
                /** @noinspection ForeachSourceInspection */
1590 6
                foreach ($var as $k => &$v) {
1591 6
                    $v = self::filter($v, $normalization_form, $leading_combining);
1592
                }
1593 6
                unset($v);
1594
1595 6
                break;
1596 62
            case 'object':
1597
                /** @noinspection ForeachSourceInspection */
1598 4
                foreach ($var as $k => &$v) {
1599 4
                    $v = self::filter($v, $normalization_form, $leading_combining);
1600
                }
1601 4
                unset($v);
1602
1603 4
                break;
1604 62
            case 'string':
1605
1606 62
                if (\strpos($var, "\r") !== false) {
1607
                    // Workaround https://bugs.php.net/65732
1608 3
                    $var = self::normalize_line_ending($var);
1609
                }
1610
1611 62
                if (ASCII::is_ascii($var) === false) {
1612 32
                    if (\Normalizer::isNormalized($var, $normalization_form)) {
1613 27
                        $n = '-';
1614
                    } else {
1615 12
                        $n = \Normalizer::normalize($var, $normalization_form);
1616
1617 12
                        if (isset($n[0])) {
1618 7
                            $var = $n;
1619
                        } else {
1620 8
                            $var = self::encode('UTF-8', $var, true);
1621
                        }
1622
                    }
1623
1624
                    if (
1625 32
                        $var[0] >= "\x80"
1626
                        &&
1627 32
                        isset($n[0], $leading_combining[0])
1628
                        &&
1629 32
                        \preg_match('/^\\p{Mn}/u', $var)
1630
                    ) {
1631
                        // Prevent leading combining chars
1632
                        // for NFC-safe concatenations.
1633 3
                        $var = $leading_combining . $var;
1634
                    }
1635
                }
1636
1637 62
                break;
1638
        }
1639
1640 62
        return $var;
1641
    }
1642
1643
    /**
1644
     * "filter_input()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1645
     *
1646
     * Gets a specific external variable by name and optionally filters it
1647
     *
1648
     * @see http://php.net/manual/en/function.filter-input.php
1649
     *
1650
     * @param int    $type          <p>
1651
     *                              One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1652
     *                              <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1653
     *                              <b>INPUT_ENV</b>.
1654
     *                              </p>
1655
     * @param string $variable_name <p>
1656
     *                              Name of a variable to get.
1657
     *                              </p>
1658
     * @param int    $filter        [optional] <p>
1659
     *                              The ID of the filter to apply. The
1660
     *                              manual page lists the available filters.
1661
     *                              </p>
1662
     * @param mixed  $options       [optional] <p>
1663
     *                              Associative array of options or bitwise disjunction of flags. If filter
1664
     *                              accepts options, flags can be provided in "flags" field of array.
1665
     *                              </p>
1666
     *
1667
     * @return mixed Value of the requested variable on success, <b>FALSE</b> if the filter fails, or <b>NULL</b> if the
1668
     *               <i>variable_name</i> variable is not set. If the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it
1669
     *               returns <b>FALSE</b> if the variable is not set and <b>NULL</b> if the filter fails.
1670
     */
1671
    public static function filter_input(
1672
        int $type,
1673
        string $variable_name,
1674
        int $filter = \FILTER_DEFAULT,
1675
        $options = null
1676
    ) {
1677
        if (\func_num_args() < 4) {
1678
            $var = \filter_input($type, $variable_name, $filter);
1679
        } else {
1680
            $var = \filter_input($type, $variable_name, $filter, $options);
1681
        }
1682
1683
        return self::filter($var);
1684
    }
1685
1686
    /**
1687
     * "filter_input_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1688
     *
1689
     * Gets external variables and optionally filters them
1690
     *
1691
     * @see http://php.net/manual/en/function.filter-input-array.php
1692
     *
1693
     * @param int   $type       <p>
1694
     *                          One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1695
     *                          <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1696
     *                          <b>INPUT_ENV</b>.
1697
     *                          </p>
1698
     * @param mixed $definition [optional] <p>
1699
     *                          An array defining the arguments. A valid key is a string
1700
     *                          containing a variable name and a valid value is either a filter type, or an array
1701
     *                          optionally specifying the filter, flags and options. If the value is an
1702
     *                          array, valid keys are filter which specifies the
1703
     *                          filter type,
1704
     *                          flags which specifies any flags that apply to the
1705
     *                          filter, and options which specifies any options that
1706
     *                          apply to the filter. See the example below for a better understanding.
1707
     *                          </p>
1708
     *                          <p>
1709
     *                          This parameter can be also an integer holding a filter constant. Then all values in the
1710
     *                          input array are filtered by this filter.
1711
     *                          </p>
1712
     * @param bool  $add_empty  [optional] <p>
1713
     *                          Add missing keys as <b>NULL</b> to the return value.
1714
     *                          </p>
1715
     *
1716
     * @return mixed An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1717
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1718
     *               set. Or if the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it returns <b>FALSE</b> if the variable
1719
     *               is not set and <b>NULL</b> if the filter fails.
1720
     */
1721
    public static function filter_input_array(
1722
        int $type,
1723
        $definition = null,
1724
        bool $add_empty = true
1725
    ) {
1726
        if (\func_num_args() < 2) {
1727
            $a = \filter_input_array($type);
1728
        } else {
1729
            $a = \filter_input_array($type, $definition, $add_empty);
1730
        }
1731
1732
        return self::filter($a);
1733
    }
1734
1735
    /**
1736
     * "filter_var()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1737
     *
1738
     * Filters a variable with a specified filter
1739
     *
1740
     * @see http://php.net/manual/en/function.filter-var.php
1741
     *
1742
     * @param mixed $variable <p>
1743
     *                        Value to filter.
1744
     *                        </p>
1745
     * @param int   $filter   [optional] <p>
1746
     *                        The ID of the filter to apply. The
1747
     *                        manual page lists the available filters.
1748
     *                        </p>
1749
     * @param mixed $options  [optional] <p>
1750
     *                        Associative array of options or bitwise disjunction of flags. If filter
1751
     *                        accepts options, flags can be provided in "flags" field of array. For
1752
     *                        the "callback" filter, callable type should be passed. The
1753
     *                        callback must accept one argument, the value to be filtered, and return
1754
     *                        the value after filtering/sanitizing it.
1755
     *                        </p>
1756
     *                        <p>
1757
     *                        <code>
1758
     *                        // for filters that accept options, use this format
1759
     *                        $options = array(
1760
     *                        'options' => array(
1761
     *                        'default' => 3, // value to return if the filter fails
1762
     *                        // other options here
1763
     *                        'min_range' => 0
1764
     *                        ),
1765
     *                        'flags' => FILTER_FLAG_ALLOW_OCTAL,
1766
     *                        );
1767
     *                        $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
1768
     *                        // for filter that only accept flags, you can pass them directly
1769
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
1770
     *                        // for filter that only accept flags, you can also pass as an array
1771
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN,
1772
     *                        array('flags' => FILTER_NULL_ON_FAILURE));
1773
     *                        // callback validate filter
1774
     *                        function foo($value)
1775
     *                        {
1776
     *                        // Expected format: Surname, GivenNames
1777
     *                        if (strpos($value, ", ") === false) return false;
1778
     *                        list($surname, $givennames) = explode(", ", $value, 2);
1779
     *                        $empty = (empty($surname) || empty($givennames));
1780
     *                        $notstrings = (!is_string($surname) || !is_string($givennames));
1781
     *                        if ($empty || $notstrings) {
1782
     *                        return false;
1783
     *                        } else {
1784
     *                        return $value;
1785
     *                        }
1786
     *                        }
1787
     *                        $var = filter_var('Doe, Jane Sue', FILTER_CALLBACK, array('options' => 'foo'));
1788
     *                        </code>
1789
     *                        </p>
1790
     *
1791
     * @return mixed the filtered data, or <b>FALSE</b> if the filter fails
1792
     */
1793 2
    public static function filter_var(
1794
        $variable,
1795
        int $filter = \FILTER_DEFAULT,
1796
        $options = null
1797
    ) {
1798 2
        if (\func_num_args() < 3) {
1799 2
            $variable = \filter_var($variable, $filter);
1800
        } else {
1801 2
            $variable = \filter_var($variable, $filter, $options);
1802
        }
1803
1804 2
        return self::filter($variable);
1805
    }
1806
1807
    /**
1808
     * "filter_var_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1809
     *
1810
     * Gets multiple variables and optionally filters them
1811
     *
1812
     * @see http://php.net/manual/en/function.filter-var-array.php
1813
     *
1814
     * @param array $data       <p>
1815
     *                          An array with string keys containing the data to filter.
1816
     *                          </p>
1817
     * @param mixed $definition [optional] <p>
1818
     *                          An array defining the arguments. A valid key is a string
1819
     *                          containing a variable name and a valid value is either a
1820
     *                          filter type, or an
1821
     *                          array optionally specifying the filter, flags and options.
1822
     *                          If the value is an array, valid keys are filter
1823
     *                          which specifies the filter type,
1824
     *                          flags which specifies any flags that apply to the
1825
     *                          filter, and options which specifies any options that
1826
     *                          apply to the filter. See the example below for a better understanding.
1827
     *                          </p>
1828
     *                          <p>
1829
     *                          This parameter can be also an integer holding a filter constant. Then all values in the
1830
     *                          input array are filtered by this filter.
1831
     *                          </p>
1832
     * @param bool  $add_empty  [optional] <p>
1833
     *                          Add missing keys as <b>NULL</b> to the return value.
1834
     *                          </p>
1835
     *
1836
     * @return mixed an array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1837
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1838
     *               set
1839
     */
1840 2
    public static function filter_var_array(
1841
        array $data,
1842
        $definition = null,
1843
        bool $add_empty = true
1844
    ) {
1845 2
        if (\func_num_args() < 2) {
1846 2
            $a = \filter_var_array($data);
1847
        } else {
1848 2
            $a = \filter_var_array($data, $definition, $add_empty);
1849
        }
1850
1851 2
        return self::filter($a);
1852
    }
1853
1854
    /**
1855
     * Checks whether finfo is available on the server.
1856
     *
1857
     * @return bool
1858
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
1859
     */
1860
    public static function finfo_loaded(): bool
1861
    {
1862
        return \class_exists('finfo');
1863
    }
1864
1865
    /**
1866
     * Returns the first $n characters of the string.
1867
     *
1868
     * @param string $str      <p>The input string.</p>
1869
     * @param int    $n        <p>Number of characters to retrieve from the start.</p>
1870
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
1871
     *
1872
     * @return string
1873
     */
1874 13
    public static function first_char(
1875
        string $str,
1876
        int $n = 1,
1877
        string $encoding = 'UTF-8'
1878
    ): string {
1879 13
        if ($str === '' || $n <= 0) {
1880 5
            return '';
1881
        }
1882
1883 8
        if ($encoding === 'UTF-8') {
1884 4
            return (string) \mb_substr($str, 0, $n);
1885
        }
1886
1887 4
        return (string) self::substr($str, 0, $n, $encoding);
1888
    }
1889
1890
    /**
1891
     * Check if the number of Unicode characters isn't greater than the specified integer.
1892
     *
1893
     * @param string $str      the original string to be checked
1894
     * @param int    $box_size the size in number of chars to be checked against string
1895
     *
1896
     * @return bool true if string is less than or equal to $box_size, false otherwise
1897
     */
1898 2
    public static function fits_inside(string $str, int $box_size): bool
1899
    {
1900 2
        return (int) self::strlen($str) <= $box_size;
1901
    }
1902
1903
    /**
1904
     * Try to fix simple broken UTF-8 strings.
1905
     *
1906
     * INFO: Take a look at "UTF8::fix_utf8()" if you need a more advanced fix for broken UTF-8 strings.
1907
     *
1908
     * If you received an UTF-8 string that was converted from Windows-1252 as it was ISO-8859-1
1909
     * (ignoring Windows-1252 chars from 80 to 9F) use this function to fix it.
1910
     * See: http://en.wikipedia.org/wiki/Windows-1252
1911
     *
1912
     * @param string $str <p>The input string</p>
1913
     *
1914
     * @return string
1915
     */
1916 46
    public static function fix_simple_utf8(string $str): string
1917
    {
1918 46
        if ($str === '') {
1919 4
            return '';
1920
        }
1921
1922 46
        static $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = null;
1923 46
        static $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = null;
1924
1925 46
        if ($BROKEN_UTF8_TO_UTF8_KEYS_CACHE === null) {
1926 1
            if (self::$BROKEN_UTF8_FIX === null) {
1927 1
                self::$BROKEN_UTF8_FIX = self::getData('utf8_fix');
1928
            }
1929
1930 1
            $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = \array_keys(self::$BROKEN_UTF8_FIX);
1931 1
            $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = \array_values(self::$BROKEN_UTF8_FIX);
1932
        }
1933
1934 46
        return \str_replace($BROKEN_UTF8_TO_UTF8_KEYS_CACHE, $BROKEN_UTF8_TO_UTF8_VALUES_CACHE, $str);
1935
    }
1936
1937
    /**
1938
     * Fix a double (or multiple) encoded UTF8 string.
1939
     *
1940
     * @param string|string[] $str you can use a string or an array of strings
1941
     *
1942
     * @return string|string[]
1943
     *                         Will return the fixed input-"array" or
1944
     *                         the fixed input-"string"
1945
     *
1946
     * @psalm-suppress InvalidReturnType
1947
     */
1948 2
    public static function fix_utf8($str)
1949
    {
1950 2
        if (\is_array($str) === true) {
1951 2
            foreach ($str as $k => &$v) {
1952 2
                $v = self::fix_utf8($v);
1953
            }
1954 2
            unset($v);
1955
1956
            /**
1957
             * @psalm-suppress InvalidReturnStatement
1958
             */
1959 2
            return $str;
1960
        }
1961
1962 2
        $str = (string) $str;
1963 2
        $last = '';
1964 2
        while ($last !== $str) {
1965 2
            $last = $str;
1966
            /**
1967
             * @psalm-suppress PossiblyInvalidArgument
1968
             */
1969 2
            $str = self::to_utf8(
1970 2
                self::utf8_decode($str, true)
1971
            );
1972
        }
1973
1974
        /**
1975
         * @psalm-suppress InvalidReturnStatement
1976
         */
1977 2
        return $str;
1978
    }
1979
1980
    /**
1981
     * Get character of a specific character.
1982
     *
1983
     * @param string $char
1984
     *
1985
     * @return string 'RTL' or 'LTR'
1986
     */
1987 2
    public static function getCharDirection(string $char): string
1988
    {
1989 2
        if (self::$SUPPORT['intlChar'] === true) {
1990
            /** @noinspection PhpComposerExtensionStubsInspection */
1991 2
            $tmp_return = \IntlChar::charDirection($char);
1992
1993
            // from "IntlChar"-Class
1994
            $char_direction = [
1995 2
                'RTL' => [1, 13, 14, 15, 21],
1996
                'LTR' => [0, 11, 12, 20],
1997
            ];
1998
1999 2
            if (\in_array($tmp_return, $char_direction['LTR'], true)) {
2000
                return 'LTR';
2001
            }
2002
2003 2
            if (\in_array($tmp_return, $char_direction['RTL'], true)) {
2004 2
                return 'RTL';
2005
            }
2006
        }
2007
2008 2
        $c = static::chr_to_decimal($char);
2009
2010 2
        if (!($c >= 0x5be && $c <= 0x10b7f)) {
2011 2
            return 'LTR';
2012
        }
2013
2014 2
        if ($c <= 0x85e) {
2015 2
            if ($c === 0x5be ||
2016 2
                $c === 0x5c0 ||
2017 2
                $c === 0x5c3 ||
2018 2
                $c === 0x5c6 ||
2019 2
                ($c >= 0x5d0 && $c <= 0x5ea) ||
2020 2
                ($c >= 0x5f0 && $c <= 0x5f4) ||
2021 2
                $c === 0x608 ||
2022 2
                $c === 0x60b ||
2023 2
                $c === 0x60d ||
2024 2
                $c === 0x61b ||
2025 2
                ($c >= 0x61e && $c <= 0x64a) ||
2026
                ($c >= 0x66d && $c <= 0x66f) ||
2027
                ($c >= 0x671 && $c <= 0x6d5) ||
2028
                ($c >= 0x6e5 && $c <= 0x6e6) ||
2029
                ($c >= 0x6ee && $c <= 0x6ef) ||
2030
                ($c >= 0x6fa && $c <= 0x70d) ||
2031
                $c === 0x710 ||
2032
                ($c >= 0x712 && $c <= 0x72f) ||
2033
                ($c >= 0x74d && $c <= 0x7a5) ||
2034
                $c === 0x7b1 ||
2035
                ($c >= 0x7c0 && $c <= 0x7ea) ||
2036
                ($c >= 0x7f4 && $c <= 0x7f5) ||
2037
                $c === 0x7fa ||
2038
                ($c >= 0x800 && $c <= 0x815) ||
2039
                $c === 0x81a ||
2040
                $c === 0x824 ||
2041
                $c === 0x828 ||
2042
                ($c >= 0x830 && $c <= 0x83e) ||
2043
                ($c >= 0x840 && $c <= 0x858) ||
2044 2
                $c === 0x85e
2045
            ) {
2046 2
                return 'RTL';
2047
            }
2048 2
        } elseif ($c === 0x200f) {
2049
            return 'RTL';
2050 2
        } elseif ($c >= 0xfb1d) {
2051 2
            if ($c === 0xfb1d ||
2052 2
                ($c >= 0xfb1f && $c <= 0xfb28) ||
2053 2
                ($c >= 0xfb2a && $c <= 0xfb36) ||
2054 2
                ($c >= 0xfb38 && $c <= 0xfb3c) ||
2055 2
                $c === 0xfb3e ||
2056 2
                ($c >= 0xfb40 && $c <= 0xfb41) ||
2057 2
                ($c >= 0xfb43 && $c <= 0xfb44) ||
2058 2
                ($c >= 0xfb46 && $c <= 0xfbc1) ||
2059 2
                ($c >= 0xfbd3 && $c <= 0xfd3d) ||
2060 2
                ($c >= 0xfd50 && $c <= 0xfd8f) ||
2061 2
                ($c >= 0xfd92 && $c <= 0xfdc7) ||
2062 2
                ($c >= 0xfdf0 && $c <= 0xfdfc) ||
2063 2
                ($c >= 0xfe70 && $c <= 0xfe74) ||
2064 2
                ($c >= 0xfe76 && $c <= 0xfefc) ||
2065 2
                ($c >= 0x10800 && $c <= 0x10805) ||
2066 2
                $c === 0x10808 ||
2067 2
                ($c >= 0x1080a && $c <= 0x10835) ||
2068 2
                ($c >= 0x10837 && $c <= 0x10838) ||
2069 2
                $c === 0x1083c ||
2070 2
                ($c >= 0x1083f && $c <= 0x10855) ||
2071 2
                ($c >= 0x10857 && $c <= 0x1085f) ||
2072 2
                ($c >= 0x10900 && $c <= 0x1091b) ||
2073 2
                ($c >= 0x10920 && $c <= 0x10939) ||
2074 2
                $c === 0x1093f ||
2075 2
                $c === 0x10a00 ||
2076 2
                ($c >= 0x10a10 && $c <= 0x10a13) ||
2077 2
                ($c >= 0x10a15 && $c <= 0x10a17) ||
2078 2
                ($c >= 0x10a19 && $c <= 0x10a33) ||
2079 2
                ($c >= 0x10a40 && $c <= 0x10a47) ||
2080 2
                ($c >= 0x10a50 && $c <= 0x10a58) ||
2081 2
                ($c >= 0x10a60 && $c <= 0x10a7f) ||
2082 2
                ($c >= 0x10b00 && $c <= 0x10b35) ||
2083 2
                ($c >= 0x10b40 && $c <= 0x10b55) ||
2084 2
                ($c >= 0x10b58 && $c <= 0x10b72) ||
2085 2
                ($c >= 0x10b78 && $c <= 0x10b7f)
2086
            ) {
2087 2
                return 'RTL';
2088
            }
2089
        }
2090
2091 2
        return 'LTR';
2092
    }
2093
2094
    /**
2095
     * Check for php-support.
2096
     *
2097
     * @param string|null $key
2098
     *
2099
     * @return mixed
2100
     *               Return the full support-"array", if $key === null<br>
2101
     *               return bool-value, if $key is used and available<br>
2102
     *               otherwise return <strong>null</strong>
2103
     */
2104 27
    public static function getSupportInfo(string $key = null)
2105
    {
2106 27
        if ($key === null) {
2107 4
            return self::$SUPPORT;
2108
        }
2109
2110 25
        if (self::$INTL_TRANSLITERATOR_LIST === null) {
2111 1
            self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
2112
        }
2113
        // compatibility fix for old versions
2114 25
        self::$SUPPORT['intl__transliterator_list_ids'] = self::$INTL_TRANSLITERATOR_LIST;
2115
2116 25
        return self::$SUPPORT[$key] ?? null;
2117
    }
2118
2119
    /**
2120
     * Warning: this method only works for some file-types (png, jpg)
2121
     *          if you need more supported types, please use e.g. "finfo"
2122
     *
2123
     * @param string $str
2124
     * @param array  $fallback with this keys: 'ext', 'mime', 'type'
2125
     *
2126
     * @return array
2127
     *               with this keys: 'ext', 'mime', 'type'
2128
     */
2129 39
    public static function get_file_type(
2130
        string $str,
2131
        array $fallback = [
2132
            'ext'  => null,
2133
            'mime' => 'application/octet-stream',
2134
            'type' => null,
2135
        ]
2136
    ): array {
2137 39
        if ($str === '') {
2138
            return $fallback;
2139
        }
2140
2141
        /** @var false|string $str_info - needed for PhpStan (stubs error) */
2142 39
        $str_info = \substr($str, 0, 2);
2143 39
        if ($str_info === false || \strlen($str_info) !== 2) {
2144 11
            return $fallback;
2145
        }
2146
2147
        // DEBUG
2148
        //var_dump($str_info);
2149
2150
        /** @var array|false $str_info - needed for PhpStan (stubs error) */
2151 35
        $str_info = \unpack('C2chars', $str_info);
0 ignored issues
show
Bug introduced by
$str_info of type array|false is incompatible with the type string expected by parameter $data of unpack(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2151
        $str_info = \unpack('C2chars', /** @scrutinizer ignore-type */ $str_info);
Loading history...
2152 35
        if ($str_info === false) {
2153
            return $fallback;
2154
        }
2155
        /** @noinspection OffsetOperationsInspection */
2156 35
        $type_code = (int) ($str_info['chars1'] . $str_info['chars2']);
2157
2158
        // DEBUG
2159
        //var_dump($type_code);
2160
2161
        //
2162
        // info: https://en.wikipedia.org/wiki/Magic_number_%28programming%29#Format_indicator
2163
        //
2164
        switch ($type_code) {
2165
            // WARNING: do not add too simple comparisons, because of false-positive results:
2166
            //
2167
            // 3780 => 'pdf', 7790 => 'exe', 7784 => 'midi', 8075 => 'zip',
2168
            // 8297 => 'rar', 7173 => 'gif', 7373 => 'tiff' 6677 => 'bmp', ...
2169
            //
2170 35
            case 255216:
2171
                $ext = 'jpg';
2172
                $mime = 'image/jpeg';
2173
                $type = 'binary';
2174
2175
                break;
2176 35
            case 13780:
2177 7
                $ext = 'png';
2178 7
                $mime = 'image/png';
2179 7
                $type = 'binary';
2180
2181 7
                break;
2182
            default:
2183 34
                return $fallback;
2184
        }
2185
2186
        return [
2187 7
            'ext'  => $ext,
2188 7
            'mime' => $mime,
2189 7
            'type' => $type,
2190
        ];
2191
    }
2192
2193
    /**
2194
     * @param int    $length         <p>Length of the random string.</p>
2195
     * @param string $possible_chars [optional] <p>Characters string for the random selection.</p>
2196
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
2197
     *
2198
     * @return string
2199
     */
2200 1
    public static function get_random_string(
2201
        int $length,
2202
        string $possible_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
2203
        string $encoding = 'UTF-8'
2204
    ): string {
2205
        // init
2206 1
        $i = 0;
2207 1
        $str = '';
2208
2209
        //
2210
        // add random chars
2211
        //
2212
2213 1
        if ($encoding === 'UTF-8') {
2214 1
            $max_length = (int) \mb_strlen($possible_chars);
2215 1
            if ($max_length === 0) {
2216 1
                return '';
2217
            }
2218
2219 1
            while ($i < $length) {
2220
                try {
2221 1
                    $rand_int = \random_int(0, $max_length - 1);
2222
                } catch (\Exception $e) {
2223
                    /** @noinspection RandomApiMigrationInspection */
2224
                    $rand_int = \mt_rand(0, $max_length - 1);
2225
                }
2226 1
                $char = \mb_substr($possible_chars, $rand_int, 1);
2227 1
                if ($char !== false) {
2228 1
                    $str .= $char;
2229 1
                    ++$i;
2230
                }
2231
            }
2232
        } else {
2233
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2234
2235
            $max_length = (int) self::strlen($possible_chars, $encoding);
2236
            if ($max_length === 0) {
2237
                return '';
2238
            }
2239
2240
            while ($i < $length) {
2241
                try {
2242
                    $rand_int = \random_int(0, $max_length - 1);
2243
                } catch (\Exception $e) {
2244
                    /** @noinspection RandomApiMigrationInspection */
2245
                    $rand_int = \mt_rand(0, $max_length - 1);
2246
                }
2247
                $char = self::substr($possible_chars, $rand_int, 1, $encoding);
2248
                if ($char !== false) {
2249
                    $str .= $char;
2250
                    ++$i;
2251
                }
2252
            }
2253
        }
2254
2255 1
        return $str;
2256
    }
2257
2258
    /**
2259
     * @param int|string $entropy_extra [optional] <p>Extra entropy via a string or int value.</p>
2260
     * @param bool       $use_md5       [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
2261
     *
2262
     * @return string
2263
     */
2264 1
    public static function get_unique_string($entropy_extra = '', bool $use_md5 = true): string
2265
    {
2266 1
        $unique_helper = \random_int(0, \mt_getrandmax()) .
2267 1
                        \session_id() .
2268 1
                        ($_SERVER['REMOTE_ADDR'] ?? '') .
2269 1
                        ($_SERVER['SERVER_ADDR'] ?? '') .
2270 1
                        $entropy_extra;
2271
2272 1
        $unique_string = \uniqid($unique_helper, true);
2273
2274 1
        if ($use_md5) {
2275 1
            $unique_string = \md5($unique_string . $unique_helper);
2276
        }
2277
2278 1
        return $unique_string;
2279
    }
2280
2281
    /**
2282
     * alias for "UTF8::string_has_bom()"
2283
     *
2284
     * @param string $str
2285
     *
2286
     * @return bool
2287
     *
2288
     * @see UTF8::string_has_bom()
2289
     * @deprecated <p>please use "UTF8::string_has_bom()"</p>
2290
     */
2291 2
    public static function hasBom(string $str): bool
2292
    {
2293 2
        return self::string_has_bom($str);
2294
    }
2295
2296
    /**
2297
     * Returns true if the string contains a lower case char, false otherwise.
2298
     *
2299
     * @param string $str <p>The input string.</p>
2300
     *
2301
     * @return bool whether or not the string contains a lower case character
2302
     */
2303 47
    public static function has_lowercase(string $str): bool
2304
    {
2305 47
        if (self::$SUPPORT['mbstring'] === true) {
2306
            /** @noinspection PhpComposerExtensionStubsInspection */
2307 47
            return \mb_ereg_match('.*[[:lower:]]', $str);
2308
        }
2309
2310
        return self::str_matches_pattern($str, '.*[[:lower:]]');
2311
    }
2312
2313
    /**
2314
     * Returns true if the string contains an upper case char, false otherwise.
2315
     *
2316
     * @param string $str <p>The input string.</p>
2317
     *
2318
     * @return bool whether or not the string contains an upper case character
2319
     */
2320 12
    public static function has_uppercase(string $str): bool
2321
    {
2322 12
        if (self::$SUPPORT['mbstring'] === true) {
2323
            /** @noinspection PhpComposerExtensionStubsInspection */
2324 12
            return \mb_ereg_match('.*[[:upper:]]', $str);
2325
        }
2326
2327
        return self::str_matches_pattern($str, '.*[[:upper:]]');
2328
    }
2329
2330
    /**
2331
     * Converts a hexadecimal value into a UTF-8 character.
2332
     *
2333
     * @param string $hexdec <p>The hexadecimal value.</p>
2334
     *
2335
     * @return false|string one single UTF-8 character
2336
     */
2337 4
    public static function hex_to_chr(string $hexdec)
2338
    {
2339 4
        return self::decimal_to_chr(\hexdec($hexdec));
2340
    }
2341
2342
    /**
2343
     * Converts hexadecimal U+xxxx code point representation to integer.
2344
     *
2345
     * INFO: opposite to UTF8::int_to_hex()
2346
     *
2347
     * @param string $hexdec <p>The hexadecimal code point representation.</p>
2348
     *
2349
     * @return false|int the code point, or false on failure
2350
     */
2351 2
    public static function hex_to_int($hexdec)
2352
    {
2353
        // init
2354 2
        $hexdec = (string) $hexdec;
2355
2356 2
        if ($hexdec === '') {
2357 2
            return false;
2358
        }
2359
2360 2
        if (\preg_match('/^(?:\\\u|U\+|)([a-zA-Z0-9]{4,6})$/', $hexdec, $match)) {
2361 2
            return \intval($match[1], 16);
2362
        }
2363
2364 2
        return false;
2365
    }
2366
2367
    /**
2368
     * alias for "UTF8::html_entity_decode()"
2369
     *
2370
     * @param string $str
2371
     * @param int    $flags
2372
     * @param string $encoding
2373
     *
2374
     * @return string
2375
     *
2376
     * @see UTF8::html_entity_decode()
2377
     * @deprecated <p>please use "UTF8::html_entity_decode()"</p>
2378
     */
2379 2
    public static function html_decode(
2380
        string $str,
2381
        int $flags = null,
2382
        string $encoding = 'UTF-8'
2383
    ): string {
2384 2
        return self::html_entity_decode($str, $flags, $encoding);
2385
    }
2386
2387
    /**
2388
     * Converts a UTF-8 string to a series of HTML numbered entities.
2389
     *
2390
     * INFO: opposite to UTF8::html_decode()
2391
     *
2392
     * @param string $str              <p>The Unicode string to be encoded as numbered entities.</p>
2393
     * @param bool   $keep_ascii_chars [optional] <p>Keep ASCII chars.</p>
2394
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
2395
     *
2396
     * @return string HTML numbered entities
2397
     */
2398 14
    public static function html_encode(
2399
        string $str,
2400
        bool $keep_ascii_chars = false,
2401
        string $encoding = 'UTF-8'
2402
    ): string {
2403 14
        if ($str === '') {
2404 4
            return '';
2405
        }
2406
2407 14
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2408 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2409
        }
2410
2411
        // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
2412 14
        if (self::$SUPPORT['mbstring'] === true) {
2413 14
            $start_code = 0x00;
2414 14
            if ($keep_ascii_chars === true) {
2415 13
                $start_code = 0x80;
2416
            }
2417
2418 14
            if ($encoding === 'UTF-8') {
2419
                /** @var false|string|null $return - needed for PhpStan (stubs error) */
2420 14
                $return = \mb_encode_numericentity(
2421 14
                    $str,
2422 14
                    [$start_code, 0xfffff, 0, 0xfffff, 0]
2423
                );
2424 14
                if ($return !== null && $return !== false) {
2425 14
                    return $return;
2426
                }
2427
            }
2428
2429
            /** @var false|string|null $return - needed for PhpStan (stubs error) */
2430 4
            $return = \mb_encode_numericentity(
2431 4
                $str,
2432 4
                [$start_code, 0xfffff, 0, 0xfffff, 0],
2433 4
                $encoding
2434
            );
2435 4
            if ($return !== null && $return !== false) {
2436 4
                return $return;
2437
            }
2438
        }
2439
2440
        //
2441
        // fallback via vanilla php
2442
        //
2443
2444
        return \implode(
2445
            '',
2446
            \array_map(
2447
                static function (string $chr) use ($keep_ascii_chars, $encoding): string {
2448
                    return self::single_chr_html_encode($chr, $keep_ascii_chars, $encoding);
2449
                },
2450
                self::str_split($str)
2451
            )
2452
        );
2453
    }
2454
2455
    /**
2456
     * UTF-8 version of html_entity_decode()
2457
     *
2458
     * The reason we are not using html_entity_decode() by itself is because
2459
     * while it is not technically correct to leave out the semicolon
2460
     * at the end of an entity most browsers will still interpret the entity
2461
     * correctly. html_entity_decode() does not convert entities without
2462
     * semicolons, so we are left with our own little solution here. Bummer.
2463
     *
2464
     * Convert all HTML entities to their applicable characters
2465
     *
2466
     * INFO: opposite to UTF8::html_encode()
2467
     *
2468
     * @see http://php.net/manual/en/function.html-entity-decode.php
2469
     *
2470
     * @param string $str      <p>
2471
     *                         The input string.
2472
     *                         </p>
2473
     * @param int    $flags    [optional] <p>
2474
     *                         A bitmask of one or more of the following flags, which specify how to handle quotes
2475
     *                         and which document type to use. The default is ENT_COMPAT | ENT_HTML401.
2476
     *                         <table>
2477
     *                         Available <i>flags</i> constants
2478
     *                         <tr valign="top">
2479
     *                         <td>Constant Name</td>
2480
     *                         <td>Description</td>
2481
     *                         </tr>
2482
     *                         <tr valign="top">
2483
     *                         <td><b>ENT_COMPAT</b></td>
2484
     *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
2485
     *                         </tr>
2486
     *                         <tr valign="top">
2487
     *                         <td><b>ENT_QUOTES</b></td>
2488
     *                         <td>Will convert both double and single quotes.</td>
2489
     *                         </tr>
2490
     *                         <tr valign="top">
2491
     *                         <td><b>ENT_NOQUOTES</b></td>
2492
     *                         <td>Will leave both double and single quotes unconverted.</td>
2493
     *                         </tr>
2494
     *                         <tr valign="top">
2495
     *                         <td><b>ENT_HTML401</b></td>
2496
     *                         <td>
2497
     *                         Handle code as HTML 4.01.
2498
     *                         </td>
2499
     *                         </tr>
2500
     *                         <tr valign="top">
2501
     *                         <td><b>ENT_XML1</b></td>
2502
     *                         <td>
2503
     *                         Handle code as XML 1.
2504
     *                         </td>
2505
     *                         </tr>
2506
     *                         <tr valign="top">
2507
     *                         <td><b>ENT_XHTML</b></td>
2508
     *                         <td>
2509
     *                         Handle code as XHTML.
2510
     *                         </td>
2511
     *                         </tr>
2512
     *                         <tr valign="top">
2513
     *                         <td><b>ENT_HTML5</b></td>
2514
     *                         <td>
2515
     *                         Handle code as HTML 5.
2516
     *                         </td>
2517
     *                         </tr>
2518
     *                         </table>
2519
     *                         </p>
2520
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2521
     *
2522
     * @return string the decoded string
2523
     */
2524 46
    public static function html_entity_decode(
2525
        string $str,
2526
        int $flags = null,
2527
        string $encoding = 'UTF-8'
2528
    ): string {
2529
        if (
2530 46
            !isset($str[3]) // examples: &; || &x;
2531
            ||
2532 46
            \strpos($str, '&') === false // no "&"
2533
        ) {
2534 23
            return $str;
2535
        }
2536
2537 44
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2538 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2539
        }
2540
2541 44
        if ($flags === null) {
2542 10
            $flags = \ENT_QUOTES | \ENT_HTML5;
2543
        }
2544
2545
        if (
2546 44
            $encoding !== 'UTF-8'
2547
            &&
2548 44
            $encoding !== 'ISO-8859-1'
2549
            &&
2550 44
            $encoding !== 'WINDOWS-1252'
2551
            &&
2552 44
            self::$SUPPORT['mbstring'] === false
2553
        ) {
2554
            \trigger_error('UTF8::html_entity_decode() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
2555
        }
2556
2557
        do {
2558 44
            $str_compare = $str;
2559
2560
            // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
2561 44
            if (self::$SUPPORT['mbstring'] === true) {
2562 44
                if ($encoding === 'UTF-8') {
2563
                    /** @var false|string|null $strTmp - needed for PhpStan (stubs error) */
2564 44
                    $strTmp = \mb_decode_numericentity(
2565 44
                        $str,
2566 44
                        [0x80, 0xfffff, 0, 0xfffff, 0]
2567
                    );
2568
                } else {
2569
                    /** @var false|string|null $strTmp - needed for PhpStan (stubs error) */
2570 4
                    $strTmp = \mb_decode_numericentity(
2571 4
                        $str,
2572 4
                        [0x80, 0xfffff, 0, 0xfffff, 0],
2573 4
                        $encoding
2574
                    );
2575
                }
2576 44
                if ($strTmp === null || $strTmp === false) {
2577 44
                    $str = self::html_entity_decode_helper($str, $encoding);
2578
                }
2579
            } else {
2580
                $str = self::html_entity_decode_helper($str, $encoding);
2581
            }
2582
2583 44
            if (\strpos($str, '&') !== false) {
2584 44
                if (\strpos($str, '&#') !== false) {
2585
                    // decode also numeric & UTF16 two byte entities
2586 36
                    $str = (string) \preg_replace(
2587 36
                        '/(&#(?:x0*[0-9a-fA-F]{2,6}(?![0-9a-fA-F;])|(?:0*\d{2,6}(?![0-9;]))))/S',
2588 36
                        '$1;',
2589 36
                        $str
2590
                    );
2591
                }
2592
2593 44
                $str = \html_entity_decode(
2594 44
                    $str,
2595 44
                    $flags,
2596 44
                    $encoding
2597
                );
2598
            }
2599 44
        } while ($str_compare !== $str);
2600
2601 44
        return $str;
2602
    }
2603
2604
    /**
2605
     * Create a escape html version of the string via "UTF8::htmlspecialchars()".
2606
     *
2607
     * @param string $str
2608
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2609
     *
2610
     * @return string
2611
     */
2612 6
    public static function html_escape(string $str, string $encoding = 'UTF-8'): string
2613
    {
2614 6
        return self::htmlspecialchars(
2615 6
            $str,
2616 6
            \ENT_QUOTES | \ENT_SUBSTITUTE,
2617 6
            $encoding
2618
        );
2619
    }
2620
2621
    /**
2622
     * Remove empty html-tag.
2623
     *
2624
     * e.g.: <tag></tag>
2625
     *
2626
     * @param string $str
2627
     *
2628
     * @return string
2629
     */
2630 1
    public static function html_stripe_empty_tags(string $str): string
2631
    {
2632 1
        return (string) \preg_replace(
2633 1
            '/<[^\\/>]*?>\\s*?<\\/[^>]*?>/u',
2634 1
            '',
2635 1
            $str
2636
        );
2637
    }
2638
2639
    /**
2640
     * Convert all applicable characters to HTML entities: UTF-8 version of htmlentities()
2641
     *
2642
     * @see http://php.net/manual/en/function.htmlentities.php
2643
     *
2644
     * @param string $str           <p>
2645
     *                              The input string.
2646
     *                              </p>
2647
     * @param int    $flags         [optional] <p>
2648
     *                              A bitmask of one or more of the following flags, which specify how to handle
2649
     *                              quotes, invalid code unit sequences and the used document type. The default is
2650
     *                              ENT_COMPAT | ENT_HTML401.
2651
     *                              <table>
2652
     *                              Available <i>flags</i> constants
2653
     *                              <tr valign="top">
2654
     *                              <td>Constant Name</td>
2655
     *                              <td>Description</td>
2656
     *                              </tr>
2657
     *                              <tr valign="top">
2658
     *                              <td><b>ENT_COMPAT</b></td>
2659
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2660
     *                              </tr>
2661
     *                              <tr valign="top">
2662
     *                              <td><b>ENT_QUOTES</b></td>
2663
     *                              <td>Will convert both double and single quotes.</td>
2664
     *                              </tr>
2665
     *                              <tr valign="top">
2666
     *                              <td><b>ENT_NOQUOTES</b></td>
2667
     *                              <td>Will leave both double and single quotes unconverted.</td>
2668
     *                              </tr>
2669
     *                              <tr valign="top">
2670
     *                              <td><b>ENT_IGNORE</b></td>
2671
     *                              <td>
2672
     *                              Silently discard invalid code unit sequences instead of returning
2673
     *                              an empty string. Using this flag is discouraged as it
2674
     *                              may have security implications.
2675
     *                              </td>
2676
     *                              </tr>
2677
     *                              <tr valign="top">
2678
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2679
     *                              <td>
2680
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
2681
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
2682
     *                              string.
2683
     *                              </td>
2684
     *                              </tr>
2685
     *                              <tr valign="top">
2686
     *                              <td><b>ENT_DISALLOWED</b></td>
2687
     *                              <td>
2688
     *                              Replace invalid code points for the given document type with a
2689
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
2690
     *                              (otherwise) instead of leaving them as is. This may be useful, for
2691
     *                              instance, to ensure the well-formedness of XML documents with
2692
     *                              embedded external content.
2693
     *                              </td>
2694
     *                              </tr>
2695
     *                              <tr valign="top">
2696
     *                              <td><b>ENT_HTML401</b></td>
2697
     *                              <td>
2698
     *                              Handle code as HTML 4.01.
2699
     *                              </td>
2700
     *                              </tr>
2701
     *                              <tr valign="top">
2702
     *                              <td><b>ENT_XML1</b></td>
2703
     *                              <td>
2704
     *                              Handle code as XML 1.
2705
     *                              </td>
2706
     *                              </tr>
2707
     *                              <tr valign="top">
2708
     *                              <td><b>ENT_XHTML</b></td>
2709
     *                              <td>
2710
     *                              Handle code as XHTML.
2711
     *                              </td>
2712
     *                              </tr>
2713
     *                              <tr valign="top">
2714
     *                              <td><b>ENT_HTML5</b></td>
2715
     *                              <td>
2716
     *                              Handle code as HTML 5.
2717
     *                              </td>
2718
     *                              </tr>
2719
     *                              </table>
2720
     *                              </p>
2721
     * @param string $encoding      [optional] <p>
2722
     *                              Like <b>htmlspecialchars</b>,
2723
     *                              <b>htmlentities</b> takes an optional third argument
2724
     *                              <i>encoding</i> which defines encoding used in
2725
     *                              conversion.
2726
     *                              Although this argument is technically optional, you are highly
2727
     *                              encouraged to specify the correct value for your code.
2728
     *                              </p>
2729
     * @param bool   $double_encode [optional] <p>
2730
     *                              When <i>double_encode</i> is turned off PHP will not
2731
     *                              encode existing html entities. The default is to convert everything.
2732
     *                              </p>
2733
     *
2734
     * @return string
2735
     *                <p>
2736
     *                The encoded string.
2737
     *                <br><br>
2738
     *                If the input <i>string</i> contains an invalid code unit
2739
     *                sequence within the given <i>encoding</i> an empty string
2740
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
2741
     *                <b>ENT_SUBSTITUTE</b> flags are set.
2742
     *                </p>
2743
     */
2744 9
    public static function htmlentities(
2745
        string $str,
2746
        int $flags = \ENT_COMPAT,
2747
        string $encoding = 'UTF-8',
2748
        bool $double_encode = true
2749
    ): string {
2750 9
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2751 7
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2752
        }
2753
2754 9
        $str = \htmlentities(
2755 9
            $str,
2756 9
            $flags,
2757 9
            $encoding,
2758 9
            $double_encode
2759
        );
2760
2761
        /**
2762
         * PHP doesn't replace a backslash to its html entity since this is something
2763
         * that's mostly used to escape characters when inserting in a database. Since
2764
         * we're using a decent database layer, we don't need this shit and we're replacing
2765
         * the double backslashes by its' html entity equivalent.
2766
         *
2767
         * https://github.com/forkcms/library/blob/master/spoon/filter/filter.php#L303
2768
         */
2769 9
        $str = \str_replace('\\', '&#92;', $str);
2770
2771 9
        return self::html_encode($str, true, $encoding);
2772
    }
2773
2774
    /**
2775
     * Convert only special characters to HTML entities: UTF-8 version of htmlspecialchars()
2776
     *
2777
     * INFO: Take a look at "UTF8::htmlentities()"
2778
     *
2779
     * @see http://php.net/manual/en/function.htmlspecialchars.php
2780
     *
2781
     * @param string $str           <p>
2782
     *                              The string being converted.
2783
     *                              </p>
2784
     * @param int    $flags         [optional] <p>
2785
     *                              A bitmask of one or more of the following flags, which specify how to handle
2786
     *                              quotes, invalid code unit sequences and the used document type. The default is
2787
     *                              ENT_COMPAT | ENT_HTML401.
2788
     *                              <table>
2789
     *                              Available <i>flags</i> constants
2790
     *                              <tr valign="top">
2791
     *                              <td>Constant Name</td>
2792
     *                              <td>Description</td>
2793
     *                              </tr>
2794
     *                              <tr valign="top">
2795
     *                              <td><b>ENT_COMPAT</b></td>
2796
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2797
     *                              </tr>
2798
     *                              <tr valign="top">
2799
     *                              <td><b>ENT_QUOTES</b></td>
2800
     *                              <td>Will convert both double and single quotes.</td>
2801
     *                              </tr>
2802
     *                              <tr valign="top">
2803
     *                              <td><b>ENT_NOQUOTES</b></td>
2804
     *                              <td>Will leave both double and single quotes unconverted.</td>
2805
     *                              </tr>
2806
     *                              <tr valign="top">
2807
     *                              <td><b>ENT_IGNORE</b></td>
2808
     *                              <td>
2809
     *                              Silently discard invalid code unit sequences instead of returning
2810
     *                              an empty string. Using this flag is discouraged as it
2811
     *                              may have security implications.
2812
     *                              </td>
2813
     *                              </tr>
2814
     *                              <tr valign="top">
2815
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2816
     *                              <td>
2817
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
2818
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
2819
     *                              string.
2820
     *                              </td>
2821
     *                              </tr>
2822
     *                              <tr valign="top">
2823
     *                              <td><b>ENT_DISALLOWED</b></td>
2824
     *                              <td>
2825
     *                              Replace invalid code points for the given document type with a
2826
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
2827
     *                              (otherwise) instead of leaving them as is. This may be useful, for
2828
     *                              instance, to ensure the well-formedness of XML documents with
2829
     *                              embedded external content.
2830
     *                              </td>
2831
     *                              </tr>
2832
     *                              <tr valign="top">
2833
     *                              <td><b>ENT_HTML401</b></td>
2834
     *                              <td>
2835
     *                              Handle code as HTML 4.01.
2836
     *                              </td>
2837
     *                              </tr>
2838
     *                              <tr valign="top">
2839
     *                              <td><b>ENT_XML1</b></td>
2840
     *                              <td>
2841
     *                              Handle code as XML 1.
2842
     *                              </td>
2843
     *                              </tr>
2844
     *                              <tr valign="top">
2845
     *                              <td><b>ENT_XHTML</b></td>
2846
     *                              <td>
2847
     *                              Handle code as XHTML.
2848
     *                              </td>
2849
     *                              </tr>
2850
     *                              <tr valign="top">
2851
     *                              <td><b>ENT_HTML5</b></td>
2852
     *                              <td>
2853
     *                              Handle code as HTML 5.
2854
     *                              </td>
2855
     *                              </tr>
2856
     *                              </table>
2857
     *                              </p>
2858
     * @param string $encoding      [optional] <p>
2859
     *                              Defines encoding used in conversion.
2860
     *                              </p>
2861
     *                              <p>
2862
     *                              For the purposes of this function, the encodings
2863
     *                              ISO-8859-1, ISO-8859-15,
2864
     *                              UTF-8, cp866,
2865
     *                              cp1251, cp1252, and
2866
     *                              KOI8-R are effectively equivalent, provided the
2867
     *                              <i>string</i> itself is valid for the encoding, as
2868
     *                              the characters affected by <b>htmlspecialchars</b> occupy
2869
     *                              the same positions in all of these encodings.
2870
     *                              </p>
2871
     * @param bool   $double_encode [optional] <p>
2872
     *                              When <i>double_encode</i> is turned off PHP will not
2873
     *                              encode existing html entities, the default is to convert everything.
2874
     *                              </p>
2875
     *
2876
     * @return string the converted string.
2877
     *                </p>
2878
     *                <p>
2879
     *                If the input <i>string</i> contains an invalid code unit
2880
     *                sequence within the given <i>encoding</i> an empty string
2881
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
2882
     *                <b>ENT_SUBSTITUTE</b> flags are set
2883
     */
2884 8
    public static function htmlspecialchars(
2885
        string $str,
2886
        int $flags = \ENT_COMPAT,
2887
        string $encoding = 'UTF-8',
2888
        bool $double_encode = true
2889
    ): string {
2890 8
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2891 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2892
        }
2893
2894 8
        return \htmlspecialchars(
2895 8
            $str,
2896 8
            $flags,
2897 8
            $encoding,
2898 8
            $double_encode
2899
        );
2900
    }
2901
2902
    /**
2903
     * Checks whether iconv is available on the server.
2904
     *
2905
     * @return bool
2906
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
2907
     */
2908
    public static function iconv_loaded(): bool
2909
    {
2910
        return \extension_loaded('iconv');
2911
    }
2912
2913
    /**
2914
     * alias for "UTF8::decimal_to_chr()"
2915
     *
2916
     * @param mixed $int
2917
     *
2918
     * @return string
2919
     *
2920
     * @see UTF8::decimal_to_chr()
2921
     * @deprecated <p>please use "UTF8::decimal_to_chr()"</p>
2922
     */
2923 4
    public static function int_to_chr($int): string
2924
    {
2925 4
        return self::decimal_to_chr($int);
2926
    }
2927
2928
    /**
2929
     * Converts Integer to hexadecimal U+xxxx code point representation.
2930
     *
2931
     * INFO: opposite to UTF8::hex_to_int()
2932
     *
2933
     * @param int    $int    <p>The integer to be converted to hexadecimal code point.</p>
2934
     * @param string $prefix [optional]
2935
     *
2936
     * @return string the code point, or empty string on failure
2937
     */
2938 6
    public static function int_to_hex(int $int, string $prefix = 'U+'): string
2939
    {
2940 6
        $hex = \dechex($int);
2941
2942 6
        $hex = (\strlen($hex) < 4 ? \substr('0000' . $hex, -4) : $hex);
2943
2944 6
        return $prefix . $hex . '';
2945
    }
2946
2947
    /**
2948
     * Checks whether intl-char is available on the server.
2949
     *
2950
     * @return bool
2951
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
2952
     */
2953
    public static function intlChar_loaded(): bool
2954
    {
2955
        return \class_exists('IntlChar');
2956
    }
2957
2958
    /**
2959
     * Checks whether intl is available on the server.
2960
     *
2961
     * @return bool
2962
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
2963
     */
2964 5
    public static function intl_loaded(): bool
2965
    {
2966 5
        return \extension_loaded('intl');
2967
    }
2968
2969
    /**
2970
     * alias for "UTF8::is_ascii()"
2971
     *
2972
     * @param string $str
2973
     *
2974
     * @return bool
2975
     *
2976
     * @see UTF8::is_ascii()
2977
     * @deprecated <p>please use "UTF8::is_ascii()"</p>
2978
     */
2979 2
    public static function isAscii(string $str): bool
2980
    {
2981 2
        return ASCII::is_ascii($str);
2982
    }
2983
2984
    /**
2985
     * alias for "UTF8::is_base64()"
2986
     *
2987
     * @param string $str
2988
     *
2989
     * @return bool
2990
     *
2991
     * @see UTF8::is_base64()
2992
     * @deprecated <p>please use "UTF8::is_base64()"</p>
2993
     */
2994 2
    public static function isBase64($str): bool
2995
    {
2996 2
        return self::is_base64($str);
2997
    }
2998
2999
    /**
3000
     * alias for "UTF8::is_binary()"
3001
     *
3002
     * @param mixed $str
3003
     * @param bool  $strict
3004
     *
3005
     * @return bool
3006
     *
3007
     * @see UTF8::is_binary()
3008
     * @deprecated <p>please use "UTF8::is_binary()"</p>
3009
     */
3010 4
    public static function isBinary($str, $strict = false): bool
3011
    {
3012 4
        return self::is_binary($str, $strict);
3013
    }
3014
3015
    /**
3016
     * alias for "UTF8::is_bom()"
3017
     *
3018
     * @param string $utf8_chr
3019
     *
3020
     * @return bool
3021
     *
3022
     * @see UTF8::is_bom()
3023
     * @deprecated <p>please use "UTF8::is_bom()"</p>
3024
     */
3025 2
    public static function isBom(string $utf8_chr): bool
3026
    {
3027 2
        return self::is_bom($utf8_chr);
3028
    }
3029
3030
    /**
3031
     * alias for "UTF8::is_html()"
3032
     *
3033
     * @param string $str
3034
     *
3035
     * @return bool
3036
     *
3037
     * @see UTF8::is_html()
3038
     * @deprecated <p>please use "UTF8::is_html()"</p>
3039
     */
3040 2
    public static function isHtml(string $str): bool
3041
    {
3042 2
        return self::is_html($str);
3043
    }
3044
3045
    /**
3046
     * alias for "UTF8::is_json()"
3047
     *
3048
     * @param string $str
3049
     *
3050
     * @return bool
3051
     *
3052
     * @see UTF8::is_json()
3053
     * @deprecated <p>please use "UTF8::is_json()"</p>
3054
     */
3055
    public static function isJson(string $str): bool
3056
    {
3057
        return self::is_json($str);
3058
    }
3059
3060
    /**
3061
     * alias for "UTF8::is_utf16()"
3062
     *
3063
     * @param mixed $str
3064
     *
3065
     * @return false|int
3066
     *                   <strong>false</strong> if is't not UTF16,<br>
3067
     *                   <strong>1</strong> for UTF-16LE,<br>
3068
     *                   <strong>2</strong> for UTF-16BE
3069
     *
3070
     * @see UTF8::is_utf16()
3071
     * @deprecated <p>please use "UTF8::is_utf16()"</p>
3072
     */
3073 2
    public static function isUtf16($str)
3074
    {
3075 2
        return self::is_utf16($str);
3076
    }
3077
3078
    /**
3079
     * alias for "UTF8::is_utf32()"
3080
     *
3081
     * @param mixed $str
3082
     *
3083
     * @return false|int
3084
     *                   <strong>false</strong> if is't not UTF16,
3085
     *                   <strong>1</strong> for UTF-32LE,
3086
     *                   <strong>2</strong> for UTF-32BE
3087
     *
3088
     * @see UTF8::is_utf32()
3089
     * @deprecated <p>please use "UTF8::is_utf32()"</p>
3090
     */
3091 2
    public static function isUtf32($str)
3092
    {
3093 2
        return self::is_utf32($str);
3094
    }
3095
3096
    /**
3097
     * alias for "UTF8::is_utf8()"
3098
     *
3099
     * @param string $str
3100
     * @param bool   $strict
3101
     *
3102
     * @return bool
3103
     *
3104
     * @see UTF8::is_utf8()
3105
     * @deprecated <p>please use "UTF8::is_utf8()"</p>
3106
     */
3107 17
    public static function isUtf8($str, $strict = false): bool
3108
    {
3109 17
        return self::is_utf8($str, $strict);
3110
    }
3111
3112
    /**
3113
     * Returns true if the string contains only alphabetic chars, false otherwise.
3114
     *
3115
     * @param string $str
3116
     *
3117
     * @return bool
3118
     *              Whether or not $str contains only alphabetic chars
3119
     */
3120 10
    public static function is_alpha(string $str): bool
3121
    {
3122 10
        if (self::$SUPPORT['mbstring'] === true) {
3123
            /** @noinspection PhpComposerExtensionStubsInspection */
3124 10
            return \mb_ereg_match('^[[:alpha:]]*$', $str);
3125
        }
3126
3127
        return self::str_matches_pattern($str, '^[[:alpha:]]*$');
3128
    }
3129
3130
    /**
3131
     * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
3132
     *
3133
     * @param string $str
3134
     *
3135
     * @return bool
3136
     *              Whether or not $str contains only alphanumeric chars
3137
     */
3138 13
    public static function is_alphanumeric(string $str): bool
3139
    {
3140 13
        if (self::$SUPPORT['mbstring'] === true) {
3141
            /** @noinspection PhpComposerExtensionStubsInspection */
3142 13
            return \mb_ereg_match('^[[:alnum:]]*$', $str);
3143
        }
3144
3145
        return self::str_matches_pattern($str, '^[[:alnum:]]*$');
3146
    }
3147
3148
    /**
3149
     * Checks if a string is 7 bit ASCII.
3150
     *
3151
     * @param string $str <p>The string to check.</p>
3152
     *
3153
     * @return bool
3154
     *              <strong>true</strong> if it is ASCII<br>
3155
     *              <strong>false</strong> otherwise
3156
     */
3157 8
    public static function is_ascii(string $str): bool
3158
    {
3159 8
        return ASCII::is_ascii($str);
3160
    }
3161
3162
    /**
3163
     * Returns true if the string is base64 encoded, false otherwise.
3164
     *
3165
     * @param mixed|string $str                   <p>The input string.</p>
3166
     * @param bool         $empty_string_is_valid [optional] <p>Is an empty string valid base64 or not?</p>
3167
     *
3168
     * @return bool whether or not $str is base64 encoded
3169
     */
3170 16
    public static function is_base64($str, $empty_string_is_valid = false): bool
3171
    {
3172
        if (
3173 16
            $empty_string_is_valid === false
3174
            &&
3175 16
            $str === ''
3176
        ) {
3177 3
            return false;
3178
        }
3179
3180
        /**
3181
         * @psalm-suppress RedundantConditionGivenDocblockType
3182
         */
3183 15
        if (\is_string($str) === false) {
3184 2
            return false;
3185
        }
3186
3187 15
        $base64String = \base64_decode($str, true);
3188
3189 15
        return $base64String !== false && \base64_encode($base64String) === $str;
3190
    }
3191
3192
    /**
3193
     * Check if the input is binary... (is look like a hack).
3194
     *
3195
     * @param mixed $input
3196
     * @param bool  $strict
3197
     *
3198
     * @return bool
3199
     */
3200 39
    public static function is_binary($input, bool $strict = false): bool
3201
    {
3202 39
        $input = (string) $input;
3203 39
        if ($input === '') {
3204 10
            return false;
3205
        }
3206
3207 39
        if (\preg_match('~^[01]+$~', $input)) {
3208 13
            return true;
3209
        }
3210
3211 39
        $ext = self::get_file_type($input);
3212 39
        if ($ext['type'] === 'binary') {
3213 7
            return true;
3214
        }
3215
3216 38
        $test_length = \strlen($input);
3217 38
        $test_null_counting = \substr_count($input, "\x0", 0, $test_length);
3218 38
        if (($test_null_counting / $test_length) > 0.25) {
3219 15
            return true;
3220
        }
3221
3222 34
        if ($strict === true) {
3223 34
            if (self::$SUPPORT['finfo'] === false) {
3224
                throw new \RuntimeException('ext-fileinfo: is not installed');
3225
            }
3226
3227
            /** @noinspection PhpComposerExtensionStubsInspection */
3228 34
            $finfo_encoding = (new \finfo(\FILEINFO_MIME_ENCODING))->buffer($input);
3229 34
            if ($finfo_encoding && $finfo_encoding === 'binary') {
3230 15
                return true;
3231
            }
3232
        }
3233
3234 30
        return false;
3235
    }
3236
3237
    /**
3238
     * Check if the file is binary.
3239
     *
3240
     * @param string $file
3241
     *
3242
     * @return bool
3243
     */
3244 6
    public static function is_binary_file($file): bool
3245
    {
3246
        // init
3247 6
        $block = '';
3248
3249 6
        $fp = \fopen($file, 'rb');
3250 6
        if (\is_resource($fp)) {
3251 6
            $block = \fread($fp, 512);
3252 6
            \fclose($fp);
3253
        }
3254
3255 6
        if ($block === '') {
3256 2
            return false;
3257
        }
3258
3259 6
        return self::is_binary($block, true);
3260
    }
3261
3262
    /**
3263
     * Returns true if the string contains only whitespace chars, false otherwise.
3264
     *
3265
     * @param string $str
3266
     *
3267
     * @return bool
3268
     *              Whether or not $str contains only whitespace characters
3269
     */
3270 15
    public static function is_blank(string $str): bool
3271
    {
3272 15
        if (self::$SUPPORT['mbstring'] === true) {
3273
            /** @noinspection PhpComposerExtensionStubsInspection */
3274 15
            return \mb_ereg_match('^[[:space:]]*$', $str);
3275
        }
3276
3277
        return self::str_matches_pattern($str, '^[[:space:]]*$');
3278
    }
3279
3280
    /**
3281
     * Checks if the given string is equal to any "Byte Order Mark".
3282
     *
3283
     * WARNING: Use "UTF8::string_has_bom()" if you will check BOM in a string.
3284
     *
3285
     * @param string $str <p>The input string.</p>
3286
     *
3287
     * @return bool
3288
     *              <strong>true</strong> if the $utf8_chr is Byte Order Mark, <strong>false</strong> otherwise
3289
     */
3290 2
    public static function is_bom($str): bool
3291
    {
3292
        /** @noinspection PhpUnusedLocalVariableInspection */
3293 2
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
3294 2
            if ($str === $bom_string) {
3295 2
                return true;
3296
            }
3297
        }
3298
3299 2
        return false;
3300
    }
3301
3302
    /**
3303
     * Determine whether the string is considered to be empty.
3304
     *
3305
     * A variable is considered empty if it does not exist or if its value equals FALSE.
3306
     * empty() does not generate a warning if the variable does not exist.
3307
     *
3308
     * @param mixed $str
3309
     *
3310
     * @return bool whether or not $str is empty()
3311
     */
3312
    public static function is_empty($str): bool
3313
    {
3314
        return empty($str);
3315
    }
3316
3317
    /**
3318
     * Returns true if the string contains only hexadecimal chars, false otherwise.
3319
     *
3320
     * @param string $str
3321
     *
3322
     * @return bool
3323
     *              Whether or not $str contains only hexadecimal chars
3324
     */
3325 13
    public static function is_hexadecimal(string $str): bool
3326
    {
3327 13
        if (self::$SUPPORT['mbstring'] === true) {
3328
            /** @noinspection PhpComposerExtensionStubsInspection */
3329 13
            return \mb_ereg_match('^[[:xdigit:]]*$', $str);
3330
        }
3331
3332
        return self::str_matches_pattern($str, '^[[:xdigit:]]*$');
3333
    }
3334
3335
    /**
3336
     * Check if the string contains any HTML tags.
3337
     *
3338
     * @param string $str <p>The input string.</p>
3339
     *
3340
     * @return bool
3341
     */
3342 3
    public static function is_html(string $str): bool
3343
    {
3344 3
        if ($str === '') {
3345 3
            return false;
3346
        }
3347
3348
        // init
3349 3
        $matches = [];
3350
3351 3
        $str = self::emoji_encode($str); // hack for emoji support :/
3352
3353 3
        \preg_match("/<\\/?\\w+(?:(?:\\s+\\w+(?:\\s*=\\s*(?:\".*?\"|'.*?'|[^'\">\\s]+))?)*\\s*|\\s*)\\/?>/u", $str, $matches);
3354
3355 3
        return \count($matches) !== 0;
3356
    }
3357
3358
    /**
3359
     * Try to check if "$str" is a JSON-string.
3360
     *
3361
     * @param string $str                                    <p>The input string.</p>
3362
     * @param bool   $only_array_or_object_results_are_valid [optional] <p>Only array and objects are valid json results.</p>
3363
     *
3364
     * @return bool
3365
     */
3366 42
    public static function is_json(
3367
        string $str,
3368
        $only_array_or_object_results_are_valid = true
3369
    ): bool {
3370 42
        if ($str === '') {
3371 4
            return false;
3372
        }
3373
3374 40
        if (self::$SUPPORT['json'] === false) {
3375
            throw new \RuntimeException('ext-json: is not installed');
3376
        }
3377
3378 40
        $json = self::json_decode($str);
3379 40
        if ($json === null && \strtoupper($str) !== 'NULL') {
3380 18
            return false;
3381
        }
3382
3383
        if (
3384 24
            $only_array_or_object_results_are_valid === true
3385
            &&
3386 24
            \is_object($json) === false
3387
            &&
3388 24
            \is_array($json) === false
3389
        ) {
3390 5
            return false;
3391
        }
3392
3393
        /** @noinspection PhpComposerExtensionStubsInspection */
3394 19
        return \json_last_error() === \JSON_ERROR_NONE;
3395
    }
3396
3397
    /**
3398
     * @param string $str
3399
     *
3400
     * @return bool
3401
     */
3402 8
    public static function is_lowercase(string $str): bool
3403
    {
3404 8
        if (self::$SUPPORT['mbstring'] === true) {
3405
            /** @noinspection PhpComposerExtensionStubsInspection */
3406 8
            return \mb_ereg_match('^[[:lower:]]*$', $str);
3407
        }
3408
3409
        return self::str_matches_pattern($str, '^[[:lower:]]*$');
3410
    }
3411
3412
    /**
3413
     * Returns true if the string is serialized, false otherwise.
3414
     *
3415
     * @param string $str
3416
     *
3417
     * @return bool whether or not $str is serialized
3418
     */
3419 7
    public static function is_serialized(string $str): bool
3420
    {
3421 7
        if ($str === '') {
3422 1
            return false;
3423
        }
3424
3425
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
3426
        /** @noinspection UnserializeExploitsInspection */
3427 6
        return $str === 'b:0;'
3428
               ||
3429 6
               @\unserialize($str) !== false;
3430
    }
3431
3432
    /**
3433
     * Returns true if the string contains only lower case chars, false
3434
     * otherwise.
3435
     *
3436
     * @param string $str <p>The input string.</p>
3437
     *
3438
     * @return bool
3439
     *              <p>Whether or not $str contains only lower case characters.</p>
3440
     */
3441 8
    public static function is_uppercase(string $str): bool
3442
    {
3443 8
        if (self::$SUPPORT['mbstring'] === true) {
3444
            /** @noinspection PhpComposerExtensionStubsInspection */
3445 8
            return \mb_ereg_match('^[[:upper:]]*$', $str);
3446
        }
3447
3448
        return self::str_matches_pattern($str, '^[[:upper:]]*$');
3449
    }
3450
3451
    /**
3452
     * Check if the string is UTF-16.
3453
     *
3454
     * @param mixed $str                       <p>The input string.</p>
3455
     * @param bool  $check_if_string_is_binary
3456
     *
3457
     * @return false|int
3458
     *                   <strong>false</strong> if is't not UTF-16,<br>
3459
     *                   <strong>1</strong> for UTF-16LE,<br>
3460
     *                   <strong>2</strong> for UTF-16BE
3461
     */
3462 22
    public static function is_utf16($str, $check_if_string_is_binary = true)
3463
    {
3464
        // init
3465 22
        $str = (string) $str;
3466 22
        $str_chars = [];
3467
3468
        if (
3469 22
            $check_if_string_is_binary === true
3470
            &&
3471 22
            self::is_binary($str, true) === false
3472
        ) {
3473 2
            return false;
3474
        }
3475
3476 22
        if (self::$SUPPORT['mbstring'] === false) {
3477 3
            \trigger_error('UTF8::is_utf16() without mbstring may did not work correctly', \E_USER_WARNING);
3478
        }
3479
3480 22
        $str = self::remove_bom($str);
3481
3482 22
        $maybe_utf16le = 0;
3483 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16LE');
3484 22
        if ($test) {
3485 15
            $test2 = \mb_convert_encoding($test, 'UTF-16LE', 'UTF-8');
3486 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16LE');
3487 15
            if ($test3 === $test) {
3488 15
                if (\count($str_chars) === 0) {
3489 15
                    $str_chars = self::count_chars($str, true, false);
3490
                }
3491 15
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
0 ignored issues
show
Bug introduced by
The expression self::count_chars($test3) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
3492 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3493 15
                        ++$maybe_utf16le;
3494
                    }
3495
                }
3496 15
                unset($test3charEmpty);
3497
            }
3498
        }
3499
3500 22
        $maybe_utf16be = 0;
3501 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16BE');
3502 22
        if ($test) {
3503 15
            $test2 = \mb_convert_encoding($test, 'UTF-16BE', 'UTF-8');
3504 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16BE');
3505 15
            if ($test3 === $test) {
3506 15
                if (\count($str_chars) === 0) {
3507 7
                    $str_chars = self::count_chars($str, true, false);
3508
                }
3509 15
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
0 ignored issues
show
Bug introduced by
The expression self::count_chars($test3) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
3510 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3511 15
                        ++$maybe_utf16be;
3512
                    }
3513
                }
3514 15
                unset($test3charEmpty);
3515
            }
3516
        }
3517
3518 22
        if ($maybe_utf16be !== $maybe_utf16le) {
3519 7
            if ($maybe_utf16le > $maybe_utf16be) {
3520 5
                return 1;
3521
            }
3522
3523 6
            return 2;
3524
        }
3525
3526 18
        return false;
3527
    }
3528
3529
    /**
3530
     * Check if the string is UTF-32.
3531
     *
3532
     * @param mixed $str                       <p>The input string.</p>
3533
     * @param bool  $check_if_string_is_binary
3534
     *
3535
     * @return false|int
3536
     *                   <strong>false</strong> if is't not UTF-32,<br>
3537
     *                   <strong>1</strong> for UTF-32LE,<br>
3538
     *                   <strong>2</strong> for UTF-32BE
3539
     */
3540 20
    public static function is_utf32($str, $check_if_string_is_binary = true)
3541
    {
3542
        // init
3543 20
        $str = (string) $str;
3544 20
        $str_chars = [];
3545
3546
        if (
3547 20
            $check_if_string_is_binary === true
3548
            &&
3549 20
            self::is_binary($str, true) === false
3550
        ) {
3551 2
            return false;
3552
        }
3553
3554 20
        if (self::$SUPPORT['mbstring'] === false) {
3555 3
            \trigger_error('UTF8::is_utf32() without mbstring may did not work correctly', \E_USER_WARNING);
3556
        }
3557
3558 20
        $str = self::remove_bom($str);
3559
3560 20
        $maybe_utf32le = 0;
3561 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32LE');
3562 20
        if ($test) {
3563 13
            $test2 = \mb_convert_encoding($test, 'UTF-32LE', 'UTF-8');
3564 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32LE');
3565 13
            if ($test3 === $test) {
3566 13
                if (\count($str_chars) === 0) {
3567 13
                    $str_chars = self::count_chars($str, true, false);
3568
                }
3569 13
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
0 ignored issues
show
Bug introduced by
The expression self::count_chars($test3) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
3570 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3571 13
                        ++$maybe_utf32le;
3572
                    }
3573
                }
3574 13
                unset($test3charEmpty);
3575
            }
3576
        }
3577
3578 20
        $maybe_utf32be = 0;
3579 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32BE');
3580 20
        if ($test) {
3581 13
            $test2 = \mb_convert_encoding($test, 'UTF-32BE', 'UTF-8');
3582 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32BE');
3583 13
            if ($test3 === $test) {
3584 13
                if (\count($str_chars) === 0) {
3585 7
                    $str_chars = self::count_chars($str, true, false);
3586
                }
3587 13
                foreach (self::count_chars($test3) as $test3char => &$test3charEmpty) {
0 ignored issues
show
Bug introduced by
The expression self::count_chars($test3) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
3588 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3589 13
                        ++$maybe_utf32be;
3590
                    }
3591
                }
3592 13
                unset($test3charEmpty);
3593
            }
3594
        }
3595
3596 20
        if ($maybe_utf32be !== $maybe_utf32le) {
3597 3
            if ($maybe_utf32le > $maybe_utf32be) {
3598 2
                return 1;
3599
            }
3600
3601 3
            return 2;
3602
        }
3603
3604 20
        return false;
3605
    }
3606
3607
    /**
3608
     * Checks whether the passed input contains only byte sequences that appear valid UTF-8.
3609
     *
3610
     * @param int|string|string[]|null $str    <p>The input to be checked.</p>
3611
     * @param bool                     $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
3612
     *
3613
     * @return bool
3614
     */
3615 82
    public static function is_utf8($str, bool $strict = false): bool
3616
    {
3617 82
        if (\is_array($str) === true) {
3618 2
            foreach ($str as &$v) {
3619 2
                if (self::is_utf8($v, $strict) === false) {
3620 2
                    return false;
3621
                }
3622
            }
3623
3624
            return true;
3625
        }
3626
3627 82
        return self::is_utf8_string((string) $str, $strict);
3628
    }
3629
3630
    /**
3631
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
3632
     * Decodes a JSON string
3633
     *
3634
     * @see http://php.net/manual/en/function.json-decode.php
3635
     *
3636
     * @param string $json    <p>
3637
     *                        The <i>json</i> string being decoded.
3638
     *                        </p>
3639
     *                        <p>
3640
     *                        This function only works with UTF-8 encoded strings.
3641
     *                        </p>
3642
     *                        <p>PHP implements a superset of
3643
     *                        JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
3644
     *                        only supports these values when they are nested inside an array or an object.
3645
     *                        </p>
3646
     * @param bool   $assoc   [optional] <p>
3647
     *                        When <b>TRUE</b>, returned objects will be converted into
3648
     *                        associative arrays.
3649
     *                        </p>
3650
     * @param int    $depth   [optional] <p>
3651
     *                        User specified recursion depth.
3652
     *                        </p>
3653
     * @param int    $options [optional] <p>
3654
     *                        Bitmask of JSON decode options. Currently only
3655
     *                        <b>JSON_BIGINT_AS_STRING</b>
3656
     *                        is supported (default is to cast large integers as floats)
3657
     *                        </p>
3658
     *
3659
     * @return mixed
3660
     *               The value encoded in <i>json</i> in appropriate PHP type. Values true, false and
3661
     *               null (case-insensitive) are returned as <b>TRUE</b>, <b>FALSE</b> and <b>NULL</b> respectively.
3662
     *               <b>NULL</b> is returned if the <i>json</i> cannot be decoded or if the encoded data
3663
     *               is deeper than the recursion limit.
3664
     */
3665 43
    public static function json_decode(
3666
        string $json,
3667
        bool $assoc = false,
3668
        int $depth = 512,
3669
        int $options = 0
3670
    ) {
3671 43
        $json = self::filter($json);
3672
3673 43
        if (self::$SUPPORT['json'] === false) {
3674
            throw new \RuntimeException('ext-json: is not installed');
3675
        }
3676
3677
        /** @noinspection PhpComposerExtensionStubsInspection */
3678 43
        return \json_decode($json, $assoc, $depth, $options);
3679
    }
3680
3681
    /**
3682
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
3683
     * Returns the JSON representation of a value.
3684
     *
3685
     * @see http://php.net/manual/en/function.json-encode.php
3686
     *
3687
     * @param mixed $value   <p>
3688
     *                       The <i>value</i> being encoded. Can be any type except
3689
     *                       a resource.
3690
     *                       </p>
3691
     *                       <p>
3692
     *                       All string data must be UTF-8 encoded.
3693
     *                       </p>
3694
     *                       <p>PHP implements a superset of
3695
     *                       JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
3696
     *                       only supports these values when they are nested inside an array or an object.
3697
     *                       </p>
3698
     * @param int   $options [optional] <p>
3699
     *                       Bitmask consisting of <b>JSON_HEX_QUOT</b>,
3700
     *                       <b>JSON_HEX_TAG</b>,
3701
     *                       <b>JSON_HEX_AMP</b>,
3702
     *                       <b>JSON_HEX_APOS</b>,
3703
     *                       <b>JSON_NUMERIC_CHECK</b>,
3704
     *                       <b>JSON_PRETTY_PRINT</b>,
3705
     *                       <b>JSON_UNESCAPED_SLASHES</b>,
3706
     *                       <b>JSON_FORCE_OBJECT</b>,
3707
     *                       <b>JSON_UNESCAPED_UNICODE</b>. The behaviour of these
3708
     *                       constants is described on
3709
     *                       the JSON constants page.
3710
     *                       </p>
3711
     * @param int   $depth   [optional] <p>
3712
     *                       Set the maximum depth. Must be greater than zero.
3713
     *                       </p>
3714
     *
3715
     * @return false|string
3716
     *                      A JSON encoded <strong>string</strong> on success or<br>
3717
     *                      <strong>FALSE</strong> on failure
3718
     */
3719 5
    public static function json_encode($value, int $options = 0, int $depth = 512)
3720
    {
3721 5
        $value = self::filter($value);
3722
3723 5
        if (self::$SUPPORT['json'] === false) {
3724
            throw new \RuntimeException('ext-json: is not installed');
3725
        }
3726
3727
        /** @noinspection PhpComposerExtensionStubsInspection */
3728 5
        return \json_encode($value, $options, $depth);
3729
    }
3730
3731
    /**
3732
     * Checks whether JSON is available on the server.
3733
     *
3734
     * @return bool
3735
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3736
     */
3737
    public static function json_loaded(): bool
3738
    {
3739
        return \function_exists('json_decode');
3740
    }
3741
3742
    /**
3743
     * Makes string's first char lowercase.
3744
     *
3745
     * @param string      $str                           <p>The input string</p>
3746
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
3747
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
3748
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
3749
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
3750
     *
3751
     * @return string the resulting string
3752
     */
3753 46
    public static function lcfirst(
3754
        string $str,
3755
        string $encoding = 'UTF-8',
3756
        bool $clean_utf8 = false,
3757
        string $lang = null,
3758
        bool $try_to_keep_the_string_length = false
3759
    ): string {
3760 46
        if ($clean_utf8 === true) {
3761
            $str = self::clean($str);
3762
        }
3763
3764 46
        $use_mb_functions = ($lang === null && $try_to_keep_the_string_length === false);
3765
3766 46
        if ($encoding === 'UTF-8') {
3767 43
            $str_part_two = (string) \mb_substr($str, 1);
3768
3769 43
            if ($use_mb_functions === true) {
3770 43
                $str_part_one = \mb_strtolower(
3771 43
                    (string) \mb_substr($str, 0, 1)
3772
                );
3773
            } else {
3774
                $str_part_one = self::strtolower(
3775
                    (string) \mb_substr($str, 0, 1),
3776
                    $encoding,
3777
                    false,
3778
                    $lang,
3779 43
                    $try_to_keep_the_string_length
3780
                );
3781
            }
3782
        } else {
3783 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
3784
3785 3
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
3786
3787 3
            $str_part_one = self::strtolower(
3788 3
                (string) self::substr($str, 0, 1, $encoding),
3789 3
                $encoding,
3790 3
                false,
3791 3
                $lang,
3792 3
                $try_to_keep_the_string_length
3793
            );
3794
        }
3795
3796 46
        return $str_part_one . $str_part_two;
3797
    }
3798
3799
    /**
3800
     * alias for "UTF8::lcfirst()"
3801
     *
3802
     * @param string      $str
3803
     * @param string      $encoding
3804
     * @param bool        $clean_utf8
3805
     * @param string|null $lang
3806
     * @param bool        $try_to_keep_the_string_length
3807
     *
3808
     * @return string
3809
     *
3810
     * @see UTF8::lcfirst()
3811
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
3812
     */
3813 2
    public static function lcword(
3814
        string $str,
3815
        string $encoding = 'UTF-8',
3816
        bool $clean_utf8 = false,
3817
        string $lang = null,
3818
        bool $try_to_keep_the_string_length = false
3819
    ): string {
3820 2
        return self::lcfirst(
3821 2
            $str,
3822 2
            $encoding,
3823 2
            $clean_utf8,
3824 2
            $lang,
3825 2
            $try_to_keep_the_string_length
3826
        );
3827
    }
3828
3829
    /**
3830
     * Lowercase for all words in the string.
3831
     *
3832
     * @param string      $str                           <p>The input string.</p>
3833
     * @param string[]    $exceptions                    [optional] <p>Exclusion for some words.</p>
3834
     * @param string      $char_list                     [optional] <p>Additional chars that contains to words and do not start
3835
     *                                                   a new word.</p>
3836
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
3837
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
3838
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
3839
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
3840
     *
3841
     * @return string
3842
     */
3843 2
    public static function lcwords(
3844
        string $str,
3845
        array $exceptions = [],
3846
        string $char_list = '',
3847
        string $encoding = 'UTF-8',
3848
        bool $clean_utf8 = false,
3849
        string $lang = null,
3850
        bool $try_to_keep_the_string_length = false
3851
    ): string {
3852 2
        if (!$str) {
3853 2
            return '';
3854
        }
3855
3856 2
        $words = self::str_to_words($str, $char_list);
3857 2
        $use_exceptions = \count($exceptions) > 0;
3858
3859 2
        foreach ($words as &$word) {
3860 2
            if (!$word) {
3861 2
                continue;
3862
            }
3863
3864
            if (
3865 2
                $use_exceptions === false
3866
                ||
3867 2
                !\in_array($word, $exceptions, true)
3868
            ) {
3869 2
                $word = self::lcfirst($word, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
3870
            }
3871
        }
3872
3873 2
        return \implode('', $words);
3874
    }
3875
3876
    /**
3877
     * alias for "UTF8::lcfirst()"
3878
     *
3879
     * @param string      $str
3880
     * @param string      $encoding
3881
     * @param bool        $clean_utf8
3882
     * @param string|null $lang
3883
     * @param bool        $try_to_keep_the_string_length
3884
     *
3885
     * @return string
3886
     *
3887
     * @see UTF8::lcfirst()
3888
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
3889
     */
3890 5
    public static function lowerCaseFirst(
3891
        string $str,
3892
        string $encoding = 'UTF-8',
3893
        bool $clean_utf8 = false,
3894
        string $lang = null,
3895
        bool $try_to_keep_the_string_length = false
3896
    ): string {
3897 5
        return self::lcfirst(
3898 5
            $str,
3899 5
            $encoding,
3900 5
            $clean_utf8,
3901 5
            $lang,
3902 5
            $try_to_keep_the_string_length
3903
        );
3904
    }
3905
3906
    /**
3907
     * Strip whitespace or other characters from the beginning of a UTF-8 string.
3908
     *
3909
     * @param string      $str   <p>The string to be trimmed</p>
3910
     * @param string|null $chars <p>Optional characters to be stripped</p>
3911
     *
3912
     * @return string the string with unwanted characters stripped from the left
3913
     */
3914 22
    public static function ltrim(string $str = '', string $chars = null): string
3915
    {
3916 22
        if ($str === '') {
3917 3
            return '';
3918
        }
3919
3920 21
        if (self::$SUPPORT['mbstring'] === true) {
3921
3922 21
            if ($chars) {
3923
                /** @noinspection PregQuoteUsageInspection */
3924 10
                $chars = \preg_quote($chars);
3925 10
                $pattern = "^[${chars}]+";
3926
            } else {
3927 14
                $pattern = '^[\\s]+';
3928
            }
3929
3930
            /** @noinspection PhpComposerExtensionStubsInspection */
3931 21
            return (string) \mb_ereg_replace($pattern, '', $str);
3932
        }
3933
3934
        if ($chars) {
3935
            $chars = \preg_quote($chars, '/');
3936
            $pattern = "^[${chars}]+";
3937
        } else {
3938
            $pattern = '^[\\s]+';
3939
        }
3940
3941
        return self::regex_replace($str, $pattern, '', '', '/');
3942
    }
3943
3944
    /**
3945
     * Returns the UTF-8 character with the maximum code point in the given data.
3946
     *
3947
     * @param array<string>|string $arg <p>A UTF-8 encoded string or an array of such strings.</p>
3948
     *
3949
     * @return string|null the character with the highest code point than others, returns null on failure or empty input
3950
     */
3951
    public static function max($arg)
3952
    {
3953 2
        if (\is_array($arg) === true) {
3954 2
            $arg = \implode('', $arg);
3955
        }
3956
3957 2
        $codepoints = self::codepoints($arg, false);
3958 2
        if (\count($codepoints) === 0) {
3959 2
            return null;
3960
        }
3961
3962 2
        $codepoint_max = \max($codepoints);
3963
3964 2
        return self::chr($codepoint_max);
3965
    }
3966
3967
    /**
3968
     * Calculates and returns the maximum number of bytes taken by any
3969
     * UTF-8 encoded character in the given string.
3970
     *
3971
     * @param string $str <p>The original Unicode string.</p>
3972
     *
3973
     * @return int max byte lengths of the given chars
3974
     */
3975
    public static function max_chr_width(string $str): int
3976
    {
3977 2
        $bytes = self::chr_size_list($str);
3978 2
        if (\count($bytes) > 0) {
3979 2
            return (int) \max($bytes);
3980
        }
3981
3982 2
        return 0;
3983
    }
3984
3985
    /**
3986
     * Checks whether mbstring is available on the server.
3987
     *
3988
     * @return bool
3989
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3990
     */
3991
    public static function mbstring_loaded(): bool
3992
    {
3993 28
        return \extension_loaded('mbstring');
3994
    }
3995
3996
    /**
3997
     * Returns the UTF-8 character with the minimum code point in the given data.
3998
     *
3999
     * @param mixed $arg <strong>A UTF-8 encoded string or an array of such strings.</strong>
4000
     *
4001
     * @return string|null the character with the lowest code point than others, returns null on failure or empty input
4002
     */
4003
    public static function min($arg)
4004
    {
4005 2
        if (\is_array($arg) === true) {
4006 2
            $arg = \implode('', $arg);
4007
        }
4008
4009 2
        $codepoints = self::codepoints($arg, false);
4010 2
        if (\count($codepoints) === 0) {
4011 2
            return null;
4012
        }
4013
4014 2
        $codepoint_min = \min($codepoints);
4015
4016 2
        return self::chr($codepoint_min);
4017
    }
4018
4019
    /**
4020
     * alias for "UTF8::normalize_encoding()"
4021
     *
4022
     * @param mixed $encoding
4023
     * @param mixed $fallback
4024
     *
4025
     * @return mixed
4026
     *
4027
     * @see UTF8::normalize_encoding()
4028
     * @deprecated <p>please use "UTF8::normalize_encoding()"</p>
4029
     */
4030
    public static function normalizeEncoding($encoding, $fallback = '')
4031
    {
4032 2
        return self::normalize_encoding($encoding, $fallback);
4033
    }
4034
4035
    /**
4036
     * Normalize the encoding-"name" input.
4037
     *
4038
     * @param mixed $encoding <p>e.g.: ISO, UTF8, WINDOWS-1251 etc.</p>
4039
     * @param mixed $fallback <p>e.g.: UTF-8</p>
4040
     *
4041
     * @return mixed e.g.: ISO-8859-1, UTF-8, WINDOWS-1251 etc.<br>Will return a empty string as fallback (by default)
4042
     */
4043
    public static function normalize_encoding($encoding, $fallback = '')
4044
    {
4045 331
        static $STATIC_NORMALIZE_ENCODING_CACHE = [];
4046
4047
        // init
4048 331
        $encoding = (string) $encoding;
4049
4050 331
        if (!$encoding) {
4051 285
            return $fallback;
4052
        }
4053
4054
        if (
4055 51
            $encoding === 'UTF-8'
4056
            ||
4057 51
            $encoding === 'UTF8'
4058
        ) {
4059 26
            return 'UTF-8';
4060
        }
4061
4062
        if (
4063 43
            $encoding === '8BIT'
4064
            ||
4065 43
            $encoding === 'BINARY'
4066
        ) {
4067
            return 'CP850';
4068
        }
4069
4070
        if (
4071 43
            $encoding === 'HTML'
4072
            ||
4073 43
            $encoding === 'HTML-ENTITIES'
4074
        ) {
4075 2
            return 'HTML-ENTITIES';
4076
        }
4077
4078
        if (
4079 43
            $encoding === '1' // only a fallback, for non "strict_types" usage ...
4080
            ||
4081 43
            $encoding === '0' // only a fallback, for non "strict_types" usage ...
4082
        ) {
4083 1
            return $fallback;
4084
        }
4085
4086 42
        if (isset($STATIC_NORMALIZE_ENCODING_CACHE[$encoding])) {
4087 40
            return $STATIC_NORMALIZE_ENCODING_CACHE[$encoding];
4088
        }
4089
4090 6
        if (self::$ENCODINGS === null) {
4091 1
            self::$ENCODINGS = self::getData('encodings');
4092
        }
4093
4094 6
        if (\in_array($encoding, self::$ENCODINGS, true)) {
4095 4
            $STATIC_NORMALIZE_ENCODING_CACHE[$encoding] = $encoding;
4096
4097 4
            return $encoding;
4098
        }
4099
4100 5
        $encoding_original = $encoding;
4101 5
        $encoding = \strtoupper($encoding);
4102 5
        $encoding_upper_helper = (string) \preg_replace('/[^a-zA-Z0-9]/u', '', $encoding);
4103
4104
        $equivalences = [
4105 5
            'ISO8859'     => 'ISO-8859-1',
4106
            'ISO88591'    => 'ISO-8859-1',
4107
            'ISO'         => 'ISO-8859-1',
4108
            'LATIN'       => 'ISO-8859-1',
4109
            'LATIN1'      => 'ISO-8859-1', // Western European
4110
            'ISO88592'    => 'ISO-8859-2',
4111
            'LATIN2'      => 'ISO-8859-2', // Central European
4112
            'ISO88593'    => 'ISO-8859-3',
4113
            'LATIN3'      => 'ISO-8859-3', // Southern European
4114
            'ISO88594'    => 'ISO-8859-4',
4115
            'LATIN4'      => 'ISO-8859-4', // Northern European
4116
            'ISO88595'    => 'ISO-8859-5',
4117
            'ISO88596'    => 'ISO-8859-6', // Greek
4118
            'ISO88597'    => 'ISO-8859-7',
4119
            'ISO88598'    => 'ISO-8859-8', // Hebrew
4120
            'ISO88599'    => 'ISO-8859-9',
4121
            'LATIN5'      => 'ISO-8859-9', // Turkish
4122
            'ISO885911'   => 'ISO-8859-11',
4123
            'TIS620'      => 'ISO-8859-11', // Thai
4124
            'ISO885910'   => 'ISO-8859-10',
4125
            'LATIN6'      => 'ISO-8859-10', // Nordic
4126
            'ISO885913'   => 'ISO-8859-13',
4127
            'LATIN7'      => 'ISO-8859-13', // Baltic
4128
            'ISO885914'   => 'ISO-8859-14',
4129
            'LATIN8'      => 'ISO-8859-14', // Celtic
4130
            'ISO885915'   => 'ISO-8859-15',
4131
            'LATIN9'      => 'ISO-8859-15', // Western European (with some extra chars e.g. €)
4132
            'ISO885916'   => 'ISO-8859-16',
4133
            'LATIN10'     => 'ISO-8859-16', // Southeast European
4134
            'CP1250'      => 'WINDOWS-1250',
4135
            'WIN1250'     => 'WINDOWS-1250',
4136
            'WINDOWS1250' => 'WINDOWS-1250',
4137
            'CP1251'      => 'WINDOWS-1251',
4138
            'WIN1251'     => 'WINDOWS-1251',
4139
            'WINDOWS1251' => 'WINDOWS-1251',
4140
            'CP1252'      => 'WINDOWS-1252',
4141
            'WIN1252'     => 'WINDOWS-1252',
4142
            'WINDOWS1252' => 'WINDOWS-1252',
4143
            'CP1253'      => 'WINDOWS-1253',
4144
            'WIN1253'     => 'WINDOWS-1253',
4145
            'WINDOWS1253' => 'WINDOWS-1253',
4146
            'CP1254'      => 'WINDOWS-1254',
4147
            'WIN1254'     => 'WINDOWS-1254',
4148
            'WINDOWS1254' => 'WINDOWS-1254',
4149
            'CP1255'      => 'WINDOWS-1255',
4150
            'WIN1255'     => 'WINDOWS-1255',
4151
            'WINDOWS1255' => 'WINDOWS-1255',
4152
            'CP1256'      => 'WINDOWS-1256',
4153
            'WIN1256'     => 'WINDOWS-1256',
4154
            'WINDOWS1256' => 'WINDOWS-1256',
4155
            'CP1257'      => 'WINDOWS-1257',
4156
            'WIN1257'     => 'WINDOWS-1257',
4157
            'WINDOWS1257' => 'WINDOWS-1257',
4158
            'CP1258'      => 'WINDOWS-1258',
4159
            'WIN1258'     => 'WINDOWS-1258',
4160
            'WINDOWS1258' => 'WINDOWS-1258',
4161
            'UTF16'       => 'UTF-16',
4162
            'UTF32'       => 'UTF-32',
4163
            'UTF8'        => 'UTF-8',
4164
            'UTF'         => 'UTF-8',
4165
            'UTF7'        => 'UTF-7',
4166
            '8BIT'        => 'CP850',
4167
            'BINARY'      => 'CP850',
4168
        ];
4169
4170 5
        if (!empty($equivalences[$encoding_upper_helper])) {
4171 4
            $encoding = $equivalences[$encoding_upper_helper];
4172
        }
4173
4174 5
        $STATIC_NORMALIZE_ENCODING_CACHE[$encoding_original] = $encoding;
4175
4176 5
        return $encoding;
4177
    }
4178
4179
    /**
4180
     * Standardize line ending to unix-like.
4181
     *
4182
     * @param string $str
4183
     *
4184
     * @return string
4185
     */
4186
    public static function normalize_line_ending(string $str): string
4187
    {
4188 5
        return \str_replace(["\r\n", "\r"], "\n", $str);
4189
    }
4190
4191
    /**
4192
     * Normalize some MS Word special characters.
4193
     *
4194
     * @param string $str <p>The string to be normalized.</p>
4195
     *
4196
     * @return string
4197
     */
4198
    public static function normalize_msword(string $str): string
4199
    {
4200 10
        return ASCII::normalize_msword($str);
4201
    }
4202
4203
    /**
4204
     * Normalize the whitespace.
4205
     *
4206
     * @param string $str                        <p>The string to be normalized.</p>
4207
     * @param bool   $keep_non_breaking_space    [optional] <p>Set to true, to keep non-breaking-spaces.</p>
4208
     * @param bool   $keep_bidi_unicode_controls [optional] <p>Set to true, to keep non-printable (for the web)
4209
     *                                           bidirectional text chars.</p>
4210
     *
4211
     * @return string
4212
     */
4213
    public static function normalize_whitespace(
4214
        string $str,
4215
        bool $keep_non_breaking_space = false,
4216
        bool $keep_bidi_unicode_controls = false
4217
    ): string {
4218 61
        return ASCII::normalize_whitespace(
4219 61
            $str,
4220 61
            $keep_non_breaking_space,
4221 61
            $keep_bidi_unicode_controls
4222
        );
4223
    }
4224
4225
    /**
4226
     * Calculates Unicode code point of the given UTF-8 encoded character.
4227
     *
4228
     * INFO: opposite to UTF8::chr()
4229
     *
4230
     * @param string $chr      <p>The character of which to calculate code point.<p/>
4231
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
4232
     *
4233
     * @return int
4234
     *             Unicode code point of the given character,<br>
4235
     *             0 on invalid UTF-8 byte sequence
4236
     */
4237
    public static function ord($chr, string $encoding = 'UTF-8'): int
4238
    {
4239 30
        static $CHAR_CACHE = [];
4240
4241
        // init
4242 30
        $chr = (string) $chr;
4243
4244 30
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
4245 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4246
        }
4247
4248 30
        $cache_key = $chr . $encoding;
4249 30
        if (isset($CHAR_CACHE[$cache_key]) === true) {
4250 30
            return $CHAR_CACHE[$cache_key];
4251
        }
4252
4253
        // check again, if it's still not UTF-8
4254 12
        if ($encoding !== 'UTF-8') {
4255 3
            $chr = self::encode($encoding, $chr);
4256
        }
4257
4258 12
        if (self::$ORD === null) {
4259
            self::$ORD = self::getData('ord');
4260
        }
4261
4262 12
        if (isset(self::$ORD[$chr])) {
4263 12
            return $CHAR_CACHE[$cache_key] = self::$ORD[$chr];
4264
        }
4265
4266
        //
4267
        // fallback via "IntlChar"
4268
        //
4269
4270 6
        if (self::$SUPPORT['intlChar'] === true) {
4271
            /** @noinspection PhpComposerExtensionStubsInspection */
4272 5
            $code = \IntlChar::ord($chr);
4273 5
            if ($code) {
4274 5
                return $CHAR_CACHE[$cache_key] = $code;
4275
            }
4276
        }
4277
4278
        //
4279
        // fallback via vanilla php
4280
        //
4281
4282
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
4283 1
        $chr = \unpack('C*', (string) \substr($chr, 0, 4));
4284
        /** @noinspection OffsetOperationsInspection */
4285 1
        $code = $chr ? $chr[1] : 0;
4286
4287
        /** @noinspection OffsetOperationsInspection */
4288 1
        if ($code >= 0xF0 && isset($chr[4])) {
4289
            /** @noinspection UnnecessaryCastingInspection */
4290
            /** @noinspection OffsetOperationsInspection */
4291
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xF0) << 18) + (($chr[2] - 0x80) << 12) + (($chr[3] - 0x80) << 6) + $chr[4] - 0x80);
4292
        }
4293
4294
        /** @noinspection OffsetOperationsInspection */
4295 1
        if ($code >= 0xE0 && isset($chr[3])) {
4296
            /** @noinspection UnnecessaryCastingInspection */
4297
            /** @noinspection OffsetOperationsInspection */
4298 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xE0) << 12) + (($chr[2] - 0x80) << 6) + $chr[3] - 0x80);
4299
        }
4300
4301
        /** @noinspection OffsetOperationsInspection */
4302 1
        if ($code >= 0xC0 && isset($chr[2])) {
4303
            /** @noinspection UnnecessaryCastingInspection */
4304
            /** @noinspection OffsetOperationsInspection */
4305 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xC0) << 6) + $chr[2] - 0x80);
4306
        }
4307
4308
        return $CHAR_CACHE[$cache_key] = $code;
4309
    }
4310
4311
    /**
4312
     * Parses the string into an array (into the the second parameter).
4313
     *
4314
     * WARNING: Unlike "parse_str()", this method does not (re-)place variables in the current scope,
4315
     *          if the second parameter is not set!
4316
     *
4317
     * @see http://php.net/manual/en/function.parse-str.php
4318
     *
4319
     * @param string $str        <p>The input string.</p>
4320
     * @param array  $result     <p>The result will be returned into this reference parameter.</p>
4321
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
4322
     *
4323
     * @return bool
4324
     *              Will return <strong>false</strong> if php can't parse the string and we haven't any $result
4325
     */
4326
    public static function parse_str(string $str, &$result, bool $clean_utf8 = false): bool
4327
    {
4328 2
        if ($clean_utf8 === true) {
4329 2
            $str = self::clean($str);
4330
        }
4331
4332 2
        if (self::$SUPPORT['mbstring'] === true) {
4333 2
            $return = \mb_parse_str($str, $result);
4334
4335 2
            return $return !== false && $result !== [];
4336
        }
4337
4338
        /** @noinspection PhpVoidFunctionResultUsedInspection */
4339
        \parse_str($str, $result);
4340
4341
        return $result !== [];
4342
    }
4343
4344
    /**
4345
     * Checks if \u modifier is available that enables Unicode support in PCRE.
4346
     *
4347
     * @return bool
4348
     *              <strong>true</strong> if support is available,<br>
4349
     *              <strong>false</strong> otherwise
4350
     */
4351
    public static function pcre_utf8_support(): bool
4352
    {
4353
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
4354 102
        return (bool) @\preg_match('//u', '');
4355
    }
4356
4357
    /**
4358
     * Create an array containing a range of UTF-8 characters.
4359
     *
4360
     * @param mixed     $var1      <p>Numeric or hexadecimal code points, or a UTF-8 character to start from.</p>
4361
     * @param mixed     $var2      <p>Numeric or hexadecimal code points, or a UTF-8 character to end at.</p>
4362
     * @param bool      $use_ctype <p>use ctype to detect numeric and hexadecimal, otherwise we will use a simple "is_numeric"</p>
4363
     * @param string    $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
4364
     * @param float|int $step      [optional] <p>
4365
     *                             If a step value is given, it will be used as the
4366
     *                             increment between elements in the sequence. step
4367
     *                             should be given as a positive number. If not specified,
4368
     *                             step will default to 1.
4369
     *                             </p>
4370
     *
4371
     * @return string[]
4372
     */
4373
    public static function range(
4374
        $var1,
4375
        $var2,
4376
        bool $use_ctype = true,
4377
        string $encoding = 'UTF-8',
4378
        $step = 1
4379
    ): array {
4380 2
        if (!$var1 || !$var2) {
4381 2
            return [];
4382
        }
4383
4384 2
        if ($step !== 1) {
4385 1
            if (!\is_numeric($step)) {
0 ignored issues
show
introduced by
The condition is_numeric($step) is always true.
Loading history...
4386
                throw new \InvalidArgumentException('$step need to be a number, type given: ' . \gettype($step));
4387
            }
4388
4389 1
            if ($step <= 0) {
4390
                throw new \InvalidArgumentException('$step need to be a positive number, given: ' . $step);
4391
            }
4392
        }
4393
4394 2
        if ($use_ctype && self::$SUPPORT['ctype'] === false) {
4395
            throw new \RuntimeException('ext-ctype: is not installed');
4396
        }
4397
4398 2
        $is_digit = false;
4399 2
        $is_xdigit = false;
4400
4401
        /** @noinspection PhpComposerExtensionStubsInspection */
4402 2
        if ($use_ctype && \ctype_digit((string) $var1) && \ctype_digit((string) $var2)) {
4403 2
            $is_digit = true;
4404 2
            $start = (int) $var1;
4405 2
        } /** @noinspection PhpComposerExtensionStubsInspection */ elseif ($use_ctype && \ctype_xdigit($var1) && \ctype_xdigit($var2)) {
4406
            $is_xdigit = true;
4407
            $start = (int) self::hex_to_int($var1);
4408 2
        } elseif (!$use_ctype && \is_numeric($var1)) {
4409 1
            $start = (int) $var1;
4410
        } else {
4411 2
            $start = self::ord($var1);
4412
        }
4413
4414 2
        if (!$start) {
4415
            return [];
4416
        }
4417
4418 2
        if ($is_digit) {
4419 2
            $end = (int) $var2;
4420 2
        } elseif ($is_xdigit) {
4421
            $end = (int) self::hex_to_int($var2);
4422 2
        } elseif (!$use_ctype && \is_numeric($var2)) {
4423 1
            $end = (int) $var2;
4424
        } else {
4425 2
            $end = self::ord($var2);
4426
        }
4427
4428 2
        if (!$end) {
4429
            return [];
4430
        }
4431
4432 2
        $array = [];
4433 2
        foreach (\range($start, $end, $step) as $i) {
4434 2
            $array[] = (string) self::chr((int) $i, $encoding);
4435
        }
4436
4437 2
        return $array;
4438
    }
4439
4440
    /**
4441
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
4442
     *
4443
     * e.g:
4444
     * 'test+test'                     => 'test+test'
4445
     * 'D&#252;sseldorf'               => 'Düsseldorf'
4446
     * 'D%FCsseldorf'                  => 'Düsseldorf'
4447
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
4448
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
4449
     * 'Düsseldorf'                   => 'Düsseldorf'
4450
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
4451
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
4452
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
4453
     *
4454
     * @param string $str          <p>The input string.</p>
4455
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
4456
     *
4457
     * @return string
4458
     */
4459
    public static function rawurldecode(string $str, bool $multi_decode = true): string
4460
    {
4461 6
        if ($str === '') {
4462 4
            return '';
4463
        }
4464
4465
        if (
4466 6
            \strpos($str, '&') === false
4467
            &&
4468 6
            \strpos($str, '%') === false
4469
            &&
4470 6
            \strpos($str, '+') === false
4471
            &&
4472 6
            \strpos($str, '\u') === false
4473
        ) {
4474 4
            return self::fix_simple_utf8($str);
4475
        }
4476
4477 6
        $str = self::urldecode_unicode_helper($str);
4478
4479
        do {
4480 6
            $str_compare = $str;
4481
4482
            /**
4483
             * @psalm-suppress PossiblyInvalidArgument
4484
             */
4485 6
            $str = self::fix_simple_utf8(
4486 6
                \rawurldecode(
4487 6
                    self::html_entity_decode(
4488 6
                        self::to_utf8($str),
4489 6
                        \ENT_QUOTES | \ENT_HTML5
4490
                    )
4491
                )
4492
            );
4493 6
        } while ($multi_decode === true && $str_compare !== $str);
4494
4495 6
        return $str;
4496
    }
4497
4498
    /**
4499
     * Replaces all occurrences of $pattern in $str by $replacement.
4500
     *
4501
     * @param string $str         <p>The input string.</p>
4502
     * @param string $pattern     <p>The regular expression pattern.</p>
4503
     * @param string $replacement <p>The string to replace with.</p>
4504
     * @param string $options     [optional] <p>Matching conditions to be used.</p>
4505
     * @param string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
4506
     *
4507
     * @return string
4508
     */
4509
    public static function regex_replace(
4510
        string $str,
4511
        string $pattern,
4512
        string $replacement,
4513
        string $options = '',
4514
        string $delimiter = '/'
4515
    ): string {
4516 18
        if ($options === 'msr') {
4517 9
            $options = 'ms';
4518
        }
4519
4520
        // fallback
4521 18
        if (!$delimiter) {
4522
            $delimiter = '/';
4523
        }
4524
4525 18
        return (string) \preg_replace(
4526 18
            $delimiter . $pattern . $delimiter . 'u' . $options,
4527 18
            $replacement,
4528 18
            $str
4529
        );
4530
    }
4531
4532
    /**
4533
     * alias for "UTF8::remove_bom()"
4534
     *
4535
     * @param string $str
4536
     *
4537
     * @return string
4538
     *
4539
     * @see UTF8::remove_bom()
4540
     * @deprecated <p>please use "UTF8::remove_bom()"</p>
4541
     */
4542
    public static function removeBOM(string $str): string
4543
    {
4544
        return self::remove_bom($str);
4545
    }
4546
4547
    /**
4548
     * Remove the BOM from UTF-8 / UTF-16 / UTF-32 strings.
4549
     *
4550
     * @param string $str <p>The input string.</p>
4551
     *
4552
     * @return string string without UTF-BOM
4553
     */
4554
    public static function remove_bom(string $str): string
4555
    {
4556 55
        if ($str === '') {
4557 9
            return '';
4558
        }
4559
4560 55
        $str_length = \strlen($str);
4561 55
        foreach (self::$BOM as $bom_string => $bom_byte_length) {
4562 55
            if (\strpos($str, $bom_string, 0) === 0) {
4563
                /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
4564 11
                $str_tmp = \substr($str, $bom_byte_length, $str_length);
4565 11
                if ($str_tmp === false) {
4566
                    return '';
4567
                }
4568
4569 11
                $str_length -= (int) $bom_byte_length;
4570
4571 55
                $str = (string) $str_tmp;
4572
            }
4573
        }
4574
4575 55
        return $str;
4576
    }
4577
4578
    /**
4579
     * Removes duplicate occurrences of a string in another string.
4580
     *
4581
     * @param string          $str  <p>The base string.</p>
4582
     * @param string|string[] $what <p>String to search for in the base string.</p>
4583
     *
4584
     * @return string the result string with removed duplicates
4585
     */
4586
    public static function remove_duplicates(string $str, $what = ' '): string
4587
    {
4588 2
        if (\is_string($what) === true) {
4589 2
            $what = [$what];
4590
        }
4591
4592 2
        if (\is_array($what) === true) {
0 ignored issues
show
introduced by
The condition is_array($what) === true is always true.
Loading history...
4593
            /** @noinspection ForeachSourceInspection */
4594 2
            foreach ($what as $item) {
4595 2
                $str = (string) \preg_replace('/(' . \preg_quote($item, '/') . ')+/u', $item, $str);
4596
            }
4597
        }
4598
4599 2
        return $str;
4600
    }
4601
4602
    /**
4603
     * Remove html via "strip_tags()" from the string.
4604
     *
4605
     * @param string $str
4606
     * @param string $allowable_tags [optional] <p>You can use the optional second parameter to specify tags which should
4607
     *                               not be stripped. Default: null
4608
     *                               </p>
4609
     *
4610
     * @return string
4611
     */
4612
    public static function remove_html(string $str, string $allowable_tags = ''): string
4613
    {
4614 6
        return \strip_tags($str, $allowable_tags);
4615
    }
4616
4617
    /**
4618
     * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
4619
     *
4620
     * @param string $str
4621
     * @param string $replacement [optional] <p>Default is a empty string.</p>
4622
     *
4623
     * @return string
4624
     */
4625
    public static function remove_html_breaks(string $str, string $replacement = ''): string
4626
    {
4627 6
        return (string) \preg_replace("#/\r\n|\r|\n|<br.*/?>#isU", $replacement, $str);
4628
    }
4629
4630
    /**
4631
     * Remove invisible characters from a string.
4632
     *
4633
     * e.g.: This prevents sandwiching null characters between ascii characters, like Java\0script.
4634
     *
4635
     * copy&past from https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Common.php
4636
     *
4637
     * @param string $str
4638
     * @param bool   $url_encoded
4639
     * @param string $replacement
4640
     *
4641
     * @return string
4642
     */
4643
    public static function remove_invisible_characters(
4644
        string $str,
4645
        bool $url_encoded = true,
4646
        string $replacement = ''
4647
    ): string {
4648 89
        return ASCII::remove_invisible_characters(
4649 89
            $str,
4650 89
            $url_encoded,
4651 89
            $replacement
4652
        );
4653
    }
4654
4655
    /**
4656
     * Returns a new string with the prefix $substring removed, if present.
4657
     *
4658
     * @param string $str
4659
     * @param string $substring <p>The prefix to remove.</p>
4660
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
4661
     *
4662
     * @return string string without the prefix $substring
4663
     */
4664
    public static function remove_left(
4665
        string $str,
4666
        string $substring,
4667
        string $encoding = 'UTF-8'
4668
    ): string {
4669 12
        if ($substring && \strpos($str, $substring) === 0) {
4670 6
            if ($encoding === 'UTF-8') {
4671 4
                return (string) \mb_substr(
4672 4
                    $str,
4673 4
                    (int) \mb_strlen($substring)
4674
                );
4675
            }
4676
4677 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4678
4679 2
            return (string) self::substr(
4680 2
                $str,
4681 2
                (int) self::strlen($substring, $encoding),
4682 2
                null,
4683 2
                $encoding
4684
            );
4685
        }
4686
4687 6
        return $str;
4688
    }
4689
4690
    /**
4691
     * Returns a new string with the suffix $substring removed, if present.
4692
     *
4693
     * @param string $str
4694
     * @param string $substring <p>The suffix to remove.</p>
4695
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
4696
     *
4697
     * @return string string having a $str without the suffix $substring
4698
     */
4699
    public static function remove_right(
4700
        string $str,
4701
        string $substring,
4702
        string $encoding = 'UTF-8'
4703
    ): string {
4704 12
        if ($substring && \substr($str, -\strlen($substring)) === $substring) {
4705 6
            if ($encoding === 'UTF-8') {
4706 4
                return (string) \mb_substr(
4707 4
                    $str,
4708 4
                    0,
4709 4
                    (int) \mb_strlen($str) - (int) \mb_strlen($substring)
4710
                );
4711
            }
4712
4713 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4714
4715 2
            return (string) self::substr(
4716 2
                $str,
4717 2
                0,
4718 2
                (int) self::strlen($str, $encoding) - (int) self::strlen($substring, $encoding),
4719 2
                $encoding
4720
            );
4721
        }
4722
4723 6
        return $str;
4724
    }
4725
4726
    /**
4727
     * Replaces all occurrences of $search in $str by $replacement.
4728
     *
4729
     * @param string $str            <p>The input string.</p>
4730
     * @param string $search         <p>The needle to search for.</p>
4731
     * @param string $replacement    <p>The string to replace with.</p>
4732
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
4733
     *
4734
     * @return string string after the replacements
4735
     */
4736
    public static function replace(
4737
        string $str,
4738
        string $search,
4739
        string $replacement,
4740
        bool $case_sensitive = true
4741
    ): string {
4742 29
        if ($case_sensitive) {
4743 22
            return \str_replace($search, $replacement, $str);
4744
        }
4745
4746 7
        return self::str_ireplace($search, $replacement, $str);
4747
    }
4748
4749
    /**
4750
     * Replaces all occurrences of $search in $str by $replacement.
4751
     *
4752
     * @param string       $str            <p>The input string.</p>
4753
     * @param array        $search         <p>The elements to search for.</p>
4754
     * @param array|string $replacement    <p>The string to replace with.</p>
4755
     * @param bool         $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
4756
     *
4757
     * @return string string after the replacements
4758
     */
4759
    public static function replace_all(
4760
        string $str,
4761
        array $search,
4762
        $replacement,
4763
        bool $case_sensitive = true
4764
    ): string {
4765 30
        if ($case_sensitive) {
4766 23
            return \str_replace($search, $replacement, $str);
4767
        }
4768
4769 7
        return self::str_ireplace($search, $replacement, $str);
4770
    }
4771
4772
    /**
4773
     * Replace the diamond question mark (�) and invalid-UTF8 chars with the replacement.
4774
     *
4775
     * @param string $str                        <p>The input string</p>
4776
     * @param string $replacement_char           <p>The replacement character.</p>
4777
     * @param bool   $process_invalid_utf8_chars <p>Convert invalid UTF-8 chars </p>
4778
     *
4779
     * @return string
4780
     */
4781
    public static function replace_diamond_question_mark(
4782
        string $str,
4783
        string $replacement_char = '',
4784
        bool $process_invalid_utf8_chars = true
4785
    ): string {
4786 35
        if ($str === '') {
4787 9
            return '';
4788
        }
4789
4790 35
        if ($process_invalid_utf8_chars === true) {
4791 35
            $replacement_char_helper = $replacement_char;
4792 35
            if ($replacement_char === '') {
4793 35
                $replacement_char_helper = 'none';
4794
            }
4795
4796 35
            if (self::$SUPPORT['mbstring'] === false) {
4797
                // if there is no native support for "mbstring",
4798
                // then we need to clean the string before ...
4799
                $str = self::clean($str);
4800
            }
4801
4802 35
            $save = \mb_substitute_character();
4803 35
            \mb_substitute_character($replacement_char_helper);
4804
            // the polyfill maybe return false, so cast to string
4805 35
            $str = (string) \mb_convert_encoding($str, 'UTF-8', 'UTF-8');
4806 35
            \mb_substitute_character($save);
4807
        }
4808
4809 35
        return \str_replace(
4810
            [
4811 35
                "\xEF\xBF\xBD",
4812
                '�',
4813
            ],
4814
            [
4815 35
                $replacement_char,
4816 35
                $replacement_char,
4817
            ],
4818 35
            $str
4819
        );
4820
    }
4821
4822
    /**
4823
     * Strip whitespace or other characters from the end of a UTF-8 string.
4824
     *
4825
     * @param string      $str   <p>The string to be trimmed.</p>
4826
     * @param string|null $chars <p>Optional characters to be stripped.</p>
4827
     *
4828
     * @return string the string with unwanted characters stripped from the right
4829
     */
4830
    public static function rtrim(string $str = '', string $chars = null): string
4831
    {
4832 20
        if ($str === '') {
4833 3
            return '';
4834
        }
4835
4836 19
        if (self::$SUPPORT['mbstring'] === true) {
4837
4838 19
            if ($chars) {
4839
                /** @noinspection PregQuoteUsageInspection */
4840 8
                $chars = \preg_quote($chars);
4841 8
                $pattern = "[${chars}]+$";
4842
            } else {
4843 14
                $pattern = '[\\s]+$';
4844
            }
4845
4846
            /** @noinspection PhpComposerExtensionStubsInspection */
4847 19
            return (string) \mb_ereg_replace($pattern, '', $str);
4848
        }
4849
4850
        if ($chars) {
4851
            $chars = \preg_quote($chars, '/');
4852
            $pattern = "[${chars}]+$";
4853
        } else {
4854
            $pattern = '[\\s]+$';
4855
        }
4856
4857
        return self::regex_replace($str, $pattern, '', '', '/');
4858
    }
4859
4860
    /**
4861
     * WARNING: Print native UTF-8 support (libs), e.g. for debugging.
4862
     *
4863
     * @psalm-suppress MissingReturnType
4864
     */
4865
    public static function showSupport()
4866
    {
4867 2
        echo '<pre>';
4868 2
        foreach (self::$SUPPORT as $key => &$value) {
4869 2
            echo $key . ' - ' . \print_r($value, true) . "\n<br>";
4870
        }
4871 2
        unset($value);
4872 2
        echo '</pre>';
4873 2
    }
4874
4875
    /**
4876
     * Converts a UTF-8 character to HTML Numbered Entity like "&#123;".
4877
     *
4878
     * @param string $char             <p>The Unicode character to be encoded as numbered entity.</p>
4879
     * @param bool   $keep_ascii_chars <p>Set to <strong>true</strong> to keep ASCII chars.</>
4880
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
4881
     *
4882
     * @return string the HTML numbered entity
4883
     */
4884
    public static function single_chr_html_encode(
4885
        string $char,
4886
        bool $keep_ascii_chars = false,
4887
        string $encoding = 'UTF-8'
4888
    ): string {
4889 2
        if ($char === '') {
4890 2
            return '';
4891
        }
4892
4893
        if (
4894 2
            $keep_ascii_chars === true
4895
            &&
4896 2
            ASCII::is_ascii($char) === true
4897
        ) {
4898 2
            return $char;
4899
        }
4900
4901 2
        return '&#' . self::ord($char, $encoding) . ';';
4902
    }
4903
4904
    /**
4905
     * @param string $str
4906
     * @param int    $tab_length
4907
     *
4908
     * @return string
4909
     */
4910
    public static function spaces_to_tabs(string $str, int $tab_length = 4): string
4911
    {
4912 5
        if ($tab_length === 4) {
4913 3
            $tab = '    ';
4914 2
        } elseif ($tab_length === 2) {
4915 1
            $tab = '  ';
4916
        } else {
4917 1
            $tab = \str_repeat(' ', $tab_length);
4918
        }
4919
4920 5
        return \str_replace($tab, "\t", $str);
4921
    }
4922
4923
    /**
4924
     * alias for "UTF8::str_split()"
4925
     *
4926
     * @param string|string[] $str
4927
     * @param int             $length
4928
     * @param bool            $clean_utf8
4929
     *
4930
     * @return string[]
4931
     *
4932
     * @see UTF8::str_split()
4933
     * @deprecated <p>please use "UTF8::str_split()"</p>
4934
     */
4935
    public static function split(
4936
        $str,
4937
        int $length = 1,
4938
        bool $clean_utf8 = false
4939
    ): array {
4940 9
        return self::str_split($str, $length, $clean_utf8);
4941
    }
4942
4943
    /**
4944
     * alias for "UTF8::str_starts_with()"
4945
     *
4946
     * @param string $haystack
4947
     * @param string $needle
4948
     *
4949
     * @return bool
4950
     *
4951
     * @see UTF8::str_starts_with()
4952
     * @deprecated <p>please use "UTF8::str_starts_with()"</p>
4953
     */
4954
    public static function str_begins(string $haystack, string $needle): bool
4955
    {
4956
        return self::str_starts_with($haystack, $needle);
4957
    }
4958
4959
    /**
4960
     * Returns a camelCase version of the string. Trims surrounding spaces,
4961
     * capitalizes letters following digits, spaces, dashes and underscores,
4962
     * and removes spaces, dashes, as well as underscores.
4963
     *
4964
     * @param string      $str                           <p>The input string.</p>
4965
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
4966
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4967
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
4968
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
4969
     *
4970
     * @return string
4971
     */
4972
    public static function str_camelize(
4973
        string $str,
4974
        string $encoding = 'UTF-8',
4975
        bool $clean_utf8 = false,
4976
        string $lang = null,
4977
        bool $try_to_keep_the_string_length = false
4978
    ): string {
4979 32
        if ($clean_utf8 === true) {
4980
            $str = self::clean($str);
4981
        }
4982
4983 32
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
4984 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4985
        }
4986
4987 32
        $str = self::lcfirst(
4988 32
            \trim($str),
4989 32
            $encoding,
4990 32
            false,
4991 32
            $lang,
4992 32
            $try_to_keep_the_string_length
4993
        );
4994 32
        $str = (string) \preg_replace('/^[-_]+/', '', $str);
4995
4996 32
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
4997
4998 32
        $str = (string) \preg_replace_callback(
4999 32
            '/[-_\\s]+(.)?/u',
5000
            /**
5001
             * @param array $match
5002
             *
5003
             * @return string
5004
             */
5005
            static function (array $match) use ($use_mb_functions, $encoding, $lang, $try_to_keep_the_string_length): string {
5006 27
                if (isset($match[1])) {
5007 27
                    if ($use_mb_functions === true) {
5008 27
                        if ($encoding === 'UTF-8') {
5009 27
                            return \mb_strtoupper($match[1]);
5010
                        }
5011
5012
                        return \mb_strtoupper($match[1], $encoding);
5013
                    }
5014
5015
                    return self::strtoupper($match[1], $encoding, false, $lang, $try_to_keep_the_string_length);
5016
                }
5017
5018 1
                return '';
5019 32
            },
5020 32
            $str
5021
        );
5022
5023 32
        return (string) \preg_replace_callback(
5024 32
            '/[\\p{N}]+(.)?/u',
5025
            /**
5026
             * @param array $match
5027
             *
5028
             * @return string
5029
             */
5030
            static function (array $match) use ($use_mb_functions, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length): string {
5031 6
                if ($use_mb_functions === true) {
5032 6
                    if ($encoding === 'UTF-8') {
5033 6
                        return \mb_strtoupper($match[0]);
5034
                    }
5035
5036
                    return \mb_strtoupper($match[0], $encoding);
5037
                }
5038
5039
                return self::strtoupper($match[0], $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5040 32
            },
5041 32
            $str
5042
        );
5043
    }
5044
5045
    /**
5046
     * Returns the string with the first letter of each word capitalized,
5047
     * except for when the word is a name which shouldn't be capitalized.
5048
     *
5049
     * @param string $str
5050
     *
5051
     * @return string string with $str capitalized
5052
     */
5053
    public static function str_capitalize_name(string $str): string
5054
    {
5055 1
        return self::str_capitalize_name_helper(
5056 1
            self::str_capitalize_name_helper(
5057 1
                self::collapse_whitespace($str),
5058 1
                ' '
5059
            ),
5060 1
            '-'
5061
        );
5062
    }
5063
5064
    /**
5065
     * Returns true if the string contains $needle, false otherwise. By default
5066
     * the comparison is case-sensitive, but can be made insensitive by setting
5067
     * $case_sensitive to false.
5068
     *
5069
     * @param string $haystack       <p>The input string.</p>
5070
     * @param string $needle         <p>Substring to look for.</p>
5071
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5072
     *
5073
     * @return bool whether or not $haystack contains $needle
5074
     */
5075
    public static function str_contains(
5076
        string $haystack,
5077
        string $needle,
5078
        bool $case_sensitive = true
5079
    ): bool {
5080 21
        if ($case_sensitive) {
5081 11
            return \strpos($haystack, $needle) !== false;
5082
        }
5083
5084 10
        return \mb_stripos($haystack, $needle) !== false;
5085
    }
5086
5087
    /**
5088
     * Returns true if the string contains all $needles, false otherwise. By
5089
     * default the comparison is case-sensitive, but can be made insensitive by
5090
     * setting $case_sensitive to false.
5091
     *
5092
     * @param string $haystack       <p>The input string.</p>
5093
     * @param array  $needles        <p>SubStrings to look for.</p>
5094
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5095
     *
5096
     * @return bool whether or not $haystack contains $needle
5097
     */
5098
    public static function str_contains_all(
5099
        string $haystack,
5100
        array $needles,
5101
        bool $case_sensitive = true
5102
    ): bool {
5103 44
        if ($haystack === '' || $needles === []) {
5104 1
            return false;
5105
        }
5106
5107
        /** @noinspection LoopWhichDoesNotLoopInspection */
5108 43
        foreach ($needles as &$needle) {
5109 43
            if (!$needle) {
5110 1
                return false;
5111
            }
5112
5113 42
            if ($case_sensitive) {
5114 22
                return \strpos($haystack, $needle) !== false;
5115
            }
5116
5117 20
            return \mb_stripos($haystack, $needle) !== false;
5118
        }
5119
5120
        return true;
5121
    }
5122
5123
    /**
5124
     * Returns true if the string contains any $needles, false otherwise. By
5125
     * default the comparison is case-sensitive, but can be made insensitive by
5126
     * setting $case_sensitive to false.
5127
     *
5128
     * @param string $haystack       <p>The input string.</p>
5129
     * @param array  $needles        <p>SubStrings to look for.</p>
5130
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5131
     *
5132
     * @return bool
5133
     *              Whether or not $str contains $needle
5134
     */
5135
    public static function str_contains_any(
5136
        string $haystack,
5137
        array $needles,
5138
        bool $case_sensitive = true
5139
    ): bool {
5140 46
        if ($haystack === '' || $needles === []) {
5141 1
            return false;
5142
        }
5143
5144
        /** @noinspection LoopWhichDoesNotLoopInspection */
5145 45
        foreach ($needles as &$needle) {
5146 45
            if (!$needle) {
5147
                continue;
5148
            }
5149
5150 45
            if ($case_sensitive) {
5151 25
                if (\strpos($haystack, $needle) !== false) {
5152 14
                    return true;
5153
                }
5154
5155 13
                continue;
5156
            }
5157
5158 20
            if (\mb_stripos($haystack, $needle) !== false) {
5159 20
                return true;
5160
            }
5161
        }
5162
5163 19
        return false;
5164
    }
5165
5166
    /**
5167
     * Returns a lowercase and trimmed string separated by dashes. Dashes are
5168
     * inserted before uppercase characters (with the exception of the first
5169
     * character of the string), and in place of spaces as well as underscores.
5170
     *
5171
     * @param string $str      <p>The input string.</p>
5172
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5173
     *
5174
     * @return string
5175
     */
5176
    public static function str_dasherize(string $str, string $encoding = 'UTF-8'): string
5177
    {
5178 19
        return self::str_delimit($str, '-', $encoding);
5179
    }
5180
5181
    /**
5182
     * Returns a lowercase and trimmed string separated by the given delimiter.
5183
     * Delimiters are inserted before uppercase characters (with the exception
5184
     * of the first character of the string), and in place of spaces, dashes,
5185
     * and underscores. Alpha delimiters are not converted to lowercase.
5186
     *
5187
     * @param string      $str                           <p>The input string.</p>
5188
     * @param string      $delimiter                     <p>Sequence used to separate parts of the string.</p>
5189
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
5190
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5191
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5192
     *                                                   tr</p>
5193
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
5194
     *                                                   ß</p>
5195
     *
5196
     * @return string
5197
     */
5198
    public static function str_delimit(
5199
        string $str,
5200
        string $delimiter,
5201
        string $encoding = 'UTF-8',
5202
        bool $clean_utf8 = false,
5203
        string $lang = null,
5204
        bool $try_to_keep_the_string_length = false
5205
    ): string {
5206 49
        if (self::$SUPPORT['mbstring'] === true) {
5207
            /** @noinspection PhpComposerExtensionStubsInspection */
5208 49
            $str = (string) \mb_ereg_replace('\\B(\\p{Lu})', '-\1', \trim($str));
5209
5210 49
            $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5211 49
            if ($use_mb_functions === true && $encoding === 'UTF-8') {
5212 22
                $str = \mb_strtolower($str);
5213
            } else {
5214 27
                $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5215
            }
5216
5217
            /** @noinspection PhpComposerExtensionStubsInspection */
5218 49
            return (string) \mb_ereg_replace('[\\-_\\s]+', $delimiter, $str);
5219
        }
5220
5221
        $str = (string) \preg_replace('/\\B(\\p{Lu})/u', '-\1', \trim($str));
5222
5223
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5224
        if ($use_mb_functions === true && $encoding === 'UTF-8') {
5225
            $str = \mb_strtolower($str);
5226
        } else {
5227
            $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5228
        }
5229
5230
        return (string) \preg_replace('/[\\-_\\s]+/u', $delimiter, $str);
5231
    }
5232
5233
    /**
5234
     * Optimized "mb_detect_encoding()"-function -> with support for UTF-16 and UTF-32.
5235
     *
5236
     * @param string $str <p>The input string.</p>
5237
     *
5238
     * @return false|string
5239
     *                      The detected string-encoding e.g. UTF-8 or UTF-16BE,<br>
5240
     *                      otherwise it will return false e.g. for BINARY or not detected encoding.
5241
     */
5242
    public static function str_detect_encoding($str)
5243
    {
5244
        // init
5245 30
        $str = (string) $str;
5246
5247
        //
5248
        // 1.) check binary strings (010001001...) like UTF-16 / UTF-32 / PDF / Images / ...
5249
        //
5250
5251 30
        if (self::is_binary($str, true) === true) {
5252 11
            $is_utf32 = self::is_utf32($str, false);
5253 11
            if ($is_utf32 === 1) {
5254
                return 'UTF-32LE';
5255
            }
5256 11
            if ($is_utf32 === 2) {
5257 1
                return 'UTF-32BE';
5258
            }
5259
5260 11
            $is_utf16 = self::is_utf16($str, false);
5261 11
            if ($is_utf16 === 1) {
5262 3
                return 'UTF-16LE';
5263
            }
5264 11
            if ($is_utf16 === 2) {
5265 2
                return 'UTF-16BE';
5266
            }
5267
5268
            // is binary but not "UTF-16" or "UTF-32"
5269 9
            return false;
5270
        }
5271
5272
        //
5273
        // 2.) simple check for ASCII chars
5274
        //
5275
5276 26
        if (ASCII::is_ascii($str) === true) {
5277 10
            return 'ASCII';
5278
        }
5279
5280
        //
5281
        // 3.) simple check for UTF-8 chars
5282
        //
5283
5284 26
        if (self::is_utf8_string($str) === true) {
5285 19
            return 'UTF-8';
5286
        }
5287
5288
        //
5289
        // 4.) check via "mb_detect_encoding()"
5290
        //
5291
        // INFO: UTF-16, UTF-32, UCS2 and UCS4, encoding detection will fail always with "mb_detect_encoding()"
5292
5293
        $encoding_detecting_order = [
5294 15
            'ISO-8859-1',
5295
            'ISO-8859-2',
5296
            'ISO-8859-3',
5297
            'ISO-8859-4',
5298
            'ISO-8859-5',
5299
            'ISO-8859-6',
5300
            'ISO-8859-7',
5301
            'ISO-8859-8',
5302
            'ISO-8859-9',
5303
            'ISO-8859-10',
5304
            'ISO-8859-13',
5305
            'ISO-8859-14',
5306
            'ISO-8859-15',
5307
            'ISO-8859-16',
5308
            'WINDOWS-1251',
5309
            'WINDOWS-1252',
5310
            'WINDOWS-1254',
5311
            'CP932',
5312
            'CP936',
5313
            'CP950',
5314
            'CP866',
5315
            'CP850',
5316
            'CP51932',
5317
            'CP50220',
5318
            'CP50221',
5319
            'CP50222',
5320
            'ISO-2022-JP',
5321
            'ISO-2022-KR',
5322
            'JIS',
5323
            'JIS-ms',
5324
            'EUC-CN',
5325
            'EUC-JP',
5326
        ];
5327
5328 15
        if (self::$SUPPORT['mbstring'] === true) {
5329
            // info: do not use the symfony polyfill here
5330 15
            $encoding = \mb_detect_encoding($str, $encoding_detecting_order, true);
5331 15
            if ($encoding) {
5332 15
                return $encoding;
5333
            }
5334
        }
5335
5336
        //
5337
        // 5.) check via "iconv()"
5338
        //
5339
5340
        if (self::$ENCODINGS === null) {
5341
            self::$ENCODINGS = self::getData('encodings');
5342
        }
5343
5344
        foreach (self::$ENCODINGS as $encoding_tmp) {
5345
            // INFO: //IGNORE but still throw notice
5346
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
5347
            if ((string) @\iconv($encoding_tmp, $encoding_tmp . '//IGNORE', $str) === $str) {
5348
                return $encoding_tmp;
5349
            }
5350
        }
5351
5352
        return false;
5353
    }
5354
5355
    /**
5356
     * alias for "UTF8::str_ends_with()"
5357
     *
5358
     * @param string $haystack
5359
     * @param string $needle
5360
     *
5361
     * @return bool
5362
     *
5363
     * @see UTF8::str_ends_with()
5364
     * @deprecated <p>please use "UTF8::str_ends_with()"</p>
5365
     */
5366
    public static function str_ends(string $haystack, string $needle): bool
5367
    {
5368
        return self::str_ends_with($haystack, $needle);
5369
    }
5370
5371
    /**
5372
     * Check if the string ends with the given substring.
5373
     *
5374
     * @param string $haystack <p>The string to search in.</p>
5375
     * @param string $needle   <p>The substring to search for.</p>
5376
     *
5377
     * @return bool
5378
     */
5379
    public static function str_ends_with(string $haystack, string $needle): bool
5380
    {
5381 9
        if ($needle === '') {
5382 2
            return true;
5383
        }
5384
5385 9
        if ($haystack === '') {
5386
            return false;
5387
        }
5388
5389 9
        return \substr($haystack, -\strlen($needle)) === $needle;
5390
    }
5391
5392
    /**
5393
     * Returns true if the string ends with any of $substrings, false otherwise.
5394
     *
5395
     * - case-sensitive
5396
     *
5397
     * @param string   $str        <p>The input string.</p>
5398
     * @param string[] $substrings <p>Substrings to look for.</p>
5399
     *
5400
     * @return bool whether or not $str ends with $substring
5401
     */
5402
    public static function str_ends_with_any(string $str, array $substrings): bool
5403
    {
5404 7
        if ($substrings === []) {
5405
            return false;
5406
        }
5407
5408 7
        foreach ($substrings as &$substring) {
5409 7
            if (\substr($str, -\strlen($substring)) === $substring) {
5410 7
                return true;
5411
            }
5412
        }
5413
5414 6
        return false;
5415
    }
5416
5417
    /**
5418
     * Ensures that the string begins with $substring. If it doesn't, it's
5419
     * prepended.
5420
     *
5421
     * @param string $str       <p>The input string.</p>
5422
     * @param string $substring <p>The substring to add if not present.</p>
5423
     *
5424
     * @return string
5425
     */
5426
    public static function str_ensure_left(string $str, string $substring): string
5427
    {
5428
        if (
5429 10
            $substring !== ''
5430
            &&
5431 10
            \strpos($str, $substring) === 0
5432
        ) {
5433 6
            return $str;
5434
        }
5435
5436 4
        return $substring . $str;
5437
    }
5438
5439
    /**
5440
     * Ensures that the string ends with $substring. If it doesn't, it's appended.
5441
     *
5442
     * @param string $str       <p>The input string.</p>
5443
     * @param string $substring <p>The substring to add if not present.</p>
5444
     *
5445
     * @return string
5446
     */
5447
    public static function str_ensure_right(string $str, string $substring): string
5448
    {
5449
        if (
5450 10
            $str === ''
5451
            ||
5452 10
            $substring === ''
5453
            ||
5454 10
            \substr($str, -\strlen($substring)) !== $substring
5455
        ) {
5456 4
            $str .= $substring;
5457
        }
5458
5459 10
        return $str;
5460
    }
5461
5462
    /**
5463
     * Capitalizes the first word of the string, replaces underscores with
5464
     * spaces, and strips '_id'.
5465
     *
5466
     * @param string $str
5467
     *
5468
     * @return string
5469
     */
5470
    public static function str_humanize($str): string
5471
    {
5472 3
        $str = \str_replace(
5473
            [
5474 3
                '_id',
5475
                '_',
5476
            ],
5477
            [
5478 3
                '',
5479
                ' ',
5480
            ],
5481 3
            $str
5482
        );
5483
5484 3
        return self::ucfirst(\trim($str));
5485
    }
5486
5487
    /**
5488
     * alias for "UTF8::str_istarts_with()"
5489
     *
5490
     * @param string $haystack
5491
     * @param string $needle
5492
     *
5493
     * @return bool
5494
     *
5495
     * @see UTF8::str_istarts_with()
5496
     * @deprecated <p>please use "UTF8::str_istarts_with()"</p>
5497
     */
5498
    public static function str_ibegins(string $haystack, string $needle): bool
5499
    {
5500
        return self::str_istarts_with($haystack, $needle);
5501
    }
5502
5503
    /**
5504
     * alias for "UTF8::str_iends_with()"
5505
     *
5506
     * @param string $haystack
5507
     * @param string $needle
5508
     *
5509
     * @return bool
5510
     *
5511
     * @see UTF8::str_iends_with()
5512
     * @deprecated <p>please use "UTF8::str_iends_with()"</p>
5513
     */
5514
    public static function str_iends(string $haystack, string $needle): bool
5515
    {
5516
        return self::str_iends_with($haystack, $needle);
5517
    }
5518
5519
    /**
5520
     * Check if the string ends with the given substring, case-insensitive.
5521
     *
5522
     * @param string $haystack <p>The string to search in.</p>
5523
     * @param string $needle   <p>The substring to search for.</p>
5524
     *
5525
     * @return bool
5526
     */
5527
    public static function str_iends_with(string $haystack, string $needle): bool
5528
    {
5529 12
        if ($needle === '') {
5530 2
            return true;
5531
        }
5532
5533 12
        if ($haystack === '') {
5534
            return false;
5535
        }
5536
5537 12
        return self::strcasecmp(\substr($haystack, -\strlen($needle)), $needle) === 0;
5538
    }
5539
5540
    /**
5541
     * Returns true if the string ends with any of $substrings, false otherwise.
5542
     *
5543
     * - case-insensitive
5544
     *
5545
     * @param string   $str        <p>The input string.</p>
5546
     * @param string[] $substrings <p>Substrings to look for.</p>
5547
     *
5548
     * @return bool
5549
     *              <p>Whether or not $str ends with $substring.</p>
5550
     */
5551
    public static function str_iends_with_any(string $str, array $substrings): bool
5552
    {
5553 4
        if ($substrings === []) {
5554
            return false;
5555
        }
5556
5557 4
        foreach ($substrings as &$substring) {
5558 4
            if (self::str_iends_with($str, $substring)) {
5559 4
                return true;
5560
            }
5561
        }
5562
5563
        return false;
5564
    }
5565
5566
    /**
5567
     * Returns the index of the first occurrence of $needle in the string,
5568
     * and false if not found. Accepts an optional offset from which to begin
5569
     * the search.
5570
     *
5571
     * @param string $str      <p>The input string.</p>
5572
     * @param string $needle   <p>Substring to look for.</p>
5573
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5574
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5575
     *
5576
     * @return false|int
5577
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5578
     *
5579
     * @see UTF8::stripos()
5580
     * @deprecated <p>please use "UTF8::stripos()"</p>
5581
     */
5582
    public static function str_iindex_first(
5583
        string $str,
5584
        string $needle,
5585
        int $offset = 0,
5586
        string $encoding = 'UTF-8'
5587
    ) {
5588
        return self::stripos(
5589
            $str,
5590
            $needle,
5591
            $offset,
5592
            $encoding
5593
        );
5594
    }
5595
5596
    /**
5597
     * Returns the index of the last occurrence of $needle in the string,
5598
     * and false if not found. Accepts an optional offset from which to begin
5599
     * the search. Offsets may be negative to count from the last character
5600
     * in the string.
5601
     *
5602
     * @param string $str      <p>The input string.</p>
5603
     * @param string $needle   <p>Substring to look for.</p>
5604
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5605
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5606
     *
5607
     * @return false|int
5608
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5609
     *
5610
     * @see UTF8::strripos()
5611
     * @deprecated <p>please use "UTF8::strripos()"</p>
5612
     */
5613
    public static function str_iindex_last(
5614
        string $str,
5615
        string $needle,
5616
        int $offset = 0,
5617
        string $encoding = 'UTF-8'
5618
    ) {
5619
        return self::strripos(
5620
            $str,
5621
            $needle,
5622
            $offset,
5623
            $encoding
5624
        );
5625
    }
5626
5627
    /**
5628
     * Returns the index of the first occurrence of $needle in the string,
5629
     * and false if not found. Accepts an optional offset from which to begin
5630
     * the search.
5631
     *
5632
     * @param string $str      <p>The input string.</p>
5633
     * @param string $needle   <p>Substring to look for.</p>
5634
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5635
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5636
     *
5637
     * @return false|int
5638
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5639
     *
5640
     * @see UTF8::strpos()
5641
     * @deprecated <p>please use "UTF8::strpos()"</p>
5642
     */
5643
    public static function str_index_first(
5644
        string $str,
5645
        string $needle,
5646
        int $offset = 0,
5647
        string $encoding = 'UTF-8'
5648
    ) {
5649 10
        return self::strpos(
5650 10
            $str,
5651 10
            $needle,
5652 10
            $offset,
5653 10
            $encoding
5654
        );
5655
    }
5656
5657
    /**
5658
     * Returns the index of the last occurrence of $needle in the string,
5659
     * and false if not found. Accepts an optional offset from which to begin
5660
     * the search. Offsets may be negative to count from the last character
5661
     * in the string.
5662
     *
5663
     * @param string $str      <p>The input string.</p>
5664
     * @param string $needle   <p>Substring to look for.</p>
5665
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5666
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5667
     *
5668
     * @return false|int
5669
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5670
     *
5671
     * @see UTF8::strrpos()
5672
     * @deprecated <p>please use "UTF8::strrpos()"</p>
5673
     */
5674
    public static function str_index_last(
5675
        string $str,
5676
        string $needle,
5677
        int $offset = 0,
5678
        string $encoding = 'UTF-8'
5679
    ) {
5680 10
        return self::strrpos(
5681 10
            $str,
5682 10
            $needle,
5683 10
            $offset,
5684 10
            $encoding
5685
        );
5686
    }
5687
5688
    /**
5689
     * Inserts $substring into the string at the $index provided.
5690
     *
5691
     * @param string $str       <p>The input string.</p>
5692
     * @param string $substring <p>String to be inserted.</p>
5693
     * @param int    $index     <p>The index at which to insert the substring.</p>
5694
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
5695
     *
5696
     * @return string
5697
     */
5698
    public static function str_insert(
5699
        string $str,
5700
        string $substring,
5701
        int $index,
5702
        string $encoding = 'UTF-8'
5703
    ): string {
5704 8
        if ($encoding === 'UTF-8') {
5705 4
            $len = (int) \mb_strlen($str);
5706 4
            if ($index > $len) {
5707
                return $str;
5708
            }
5709
5710
            /** @noinspection UnnecessaryCastingInspection */
5711 4
            return (string) \mb_substr($str, 0, $index) .
5712 4
                   $substring .
5713 4
                   (string) \mb_substr($str, $index, $len);
5714
        }
5715
5716 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
5717
5718 4
        $len = (int) self::strlen($str, $encoding);
5719 4
        if ($index > $len) {
5720 1
            return $str;
5721
        }
5722
5723 3
        return ((string) self::substr($str, 0, $index, $encoding)) .
5724 3
               $substring .
5725 3
               ((string) self::substr($str, $index, $len, $encoding));
5726
    }
5727
5728
    /**
5729
     * Case-insensitive and UTF-8 safe version of <function>str_replace</function>.
5730
     *
5731
     * @see http://php.net/manual/en/function.str-ireplace.php
5732
     *
5733
     * @param mixed $search  <p>
5734
     *                       Every replacement with search array is
5735
     *                       performed on the result of previous replacement.
5736
     *                       </p>
5737
     * @param mixed $replace <p>
5738
     *                       </p>
5739
     * @param mixed $subject <p>
5740
     *                       If subject is an array, then the search and
5741
     *                       replace is performed with every entry of
5742
     *                       subject, and the return value is an array as
5743
     *                       well.
5744
     *                       </p>
5745
     * @param int   $count   [optional] <p>
5746
     *                       The number of matched and replaced needles will
5747
     *                       be returned in count which is passed by
5748
     *                       reference.
5749
     *                       </p>
5750
     *
5751
     * @return mixed a string or an array of replacements
5752
     */
5753
    public static function str_ireplace($search, $replace, $subject, &$count = null)
5754
    {
5755 29
        $search = (array) $search;
5756
5757
        /** @noinspection AlterInForeachInspection */
5758 29
        foreach ($search as &$s) {
5759 29
            $s = (string) $s;
5760 29
            if ($s === '') {
5761 6
                $s = '/^(?<=.)$/';
5762
            } else {
5763 29
                $s = '/' . \preg_quote($s, '/') . '/ui';
5764
            }
5765
        }
5766
5767 29
        $subject = \preg_replace($search, $replace, $subject, -1, $replace);
5768 29
        $count = $replace; // used as reference parameter
5769
5770 29
        return $subject;
5771
    }
5772
5773
    /**
5774
     * Replaces $search from the beginning of string with $replacement.
5775
     *
5776
     * @param string $str         <p>The input string.</p>
5777
     * @param string $search      <p>The string to search for.</p>
5778
     * @param string $replacement <p>The replacement.</p>
5779
     *
5780
     * @return string string after the replacements
5781
     */
5782
    public static function str_ireplace_beginning(string $str, string $search, string $replacement): string
5783
    {
5784 17
        if ($str === '') {
5785 4
            if ($replacement === '') {
5786 2
                return '';
5787
            }
5788
5789 2
            if ($search === '') {
5790 2
                return $replacement;
5791
            }
5792
        }
5793
5794 13
        if ($search === '') {
5795 2
            return $str . $replacement;
5796
        }
5797
5798 11
        if (\stripos($str, $search) === 0) {
5799 10
            return $replacement . \substr($str, \strlen($search));
5800
        }
5801
5802 1
        return $str;
5803
    }
5804
5805
    /**
5806
     * Replaces $search from the ending of string with $replacement.
5807
     *
5808
     * @param string $str         <p>The input string.</p>
5809
     * @param string $search      <p>The string to search for.</p>
5810
     * @param string $replacement <p>The replacement.</p>
5811
     *
5812
     * @return string string after the replacements
5813
     */
5814
    public static function str_ireplace_ending(string $str, string $search, string $replacement): string
5815
    {
5816 17
        if ($str === '') {
5817 4
            if ($replacement === '') {
5818 2
                return '';
5819
            }
5820
5821 2
            if ($search === '') {
5822 2
                return $replacement;
5823
            }
5824
        }
5825
5826 13
        if ($search === '') {
5827 2
            return $str . $replacement;
5828
        }
5829
5830 11
        if (\stripos($str, $search, \strlen($str) - \strlen($search)) !== false) {
5831 9
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
5832
        }
5833
5834 11
        return $str;
5835
    }
5836
5837
    /**
5838
     * Check if the string starts with the given substring, case-insensitive.
5839
     *
5840
     * @param string $haystack <p>The string to search in.</p>
5841
     * @param string $needle   <p>The substring to search for.</p>
5842
     *
5843
     * @return bool
5844
     */
5845
    public static function str_istarts_with(string $haystack, string $needle): bool
5846
    {
5847 12
        if ($needle === '') {
5848 2
            return true;
5849
        }
5850
5851 12
        if ($haystack === '') {
5852
            return false;
5853
        }
5854
5855 12
        return self::stripos($haystack, $needle) === 0;
5856
    }
5857
5858
    /**
5859
     * Returns true if the string begins with any of $substrings, false otherwise.
5860
     *
5861
     * - case-insensitive
5862
     *
5863
     * @param string $str        <p>The input string.</p>
5864
     * @param array  $substrings <p>Substrings to look for.</p>
5865
     *
5866
     * @return bool whether or not $str starts with $substring
5867
     */
5868
    public static function str_istarts_with_any(string $str, array $substrings): bool
5869
    {
5870 4
        if ($str === '') {
5871
            return false;
5872
        }
5873
5874 4
        if ($substrings === []) {
5875
            return false;
5876
        }
5877
5878 4
        foreach ($substrings as &$substring) {
5879 4
            if (self::str_istarts_with($str, $substring)) {
5880 4
                return true;
5881
            }
5882
        }
5883
5884
        return false;
5885
    }
5886
5887
    /**
5888
     * Gets the substring after the first occurrence of a separator.
5889
     *
5890
     * @param string $str       <p>The input string.</p>
5891
     * @param string $separator <p>The string separator.</p>
5892
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5893
     *
5894
     * @return string
5895
     */
5896
    public static function str_isubstr_after_first_separator(
5897
        string $str,
5898
        string $separator,
5899
        string $encoding = 'UTF-8'
5900
    ): string {
5901 1
        if ($separator === '' || $str === '') {
5902 1
            return '';
5903
        }
5904
5905 1
        $offset = self::stripos($str, $separator);
5906 1
        if ($offset === false) {
5907 1
            return '';
5908
        }
5909
5910 1
        if ($encoding === 'UTF-8') {
5911 1
            return (string) \mb_substr(
5912 1
                $str,
5913 1
                $offset + (int) \mb_strlen($separator)
5914
            );
5915
        }
5916
5917
        return (string) self::substr(
5918
            $str,
5919
            $offset + (int) self::strlen($separator, $encoding),
5920
            null,
5921
            $encoding
5922
        );
5923
    }
5924
5925
    /**
5926
     * Gets the substring after the last occurrence of a separator.
5927
     *
5928
     * @param string $str       <p>The input string.</p>
5929
     * @param string $separator <p>The string separator.</p>
5930
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5931
     *
5932
     * @return string
5933
     */
5934
    public static function str_isubstr_after_last_separator(
5935
        string $str,
5936
        string $separator,
5937
        string $encoding = 'UTF-8'
5938
    ): string {
5939 1
        if ($separator === '' || $str === '') {
5940 1
            return '';
5941
        }
5942
5943 1
        $offset = self::strripos($str, $separator);
5944 1
        if ($offset === false) {
5945 1
            return '';
5946
        }
5947
5948 1
        if ($encoding === 'UTF-8') {
5949 1
            return (string) \mb_substr(
5950 1
                $str,
5951 1
                $offset + (int) self::strlen($separator)
5952
            );
5953
        }
5954
5955
        return (string) self::substr(
5956
            $str,
5957
            $offset + (int) self::strlen($separator, $encoding),
5958
            null,
5959
            $encoding
5960
        );
5961
    }
5962
5963
    /**
5964
     * Gets the substring before the first occurrence of a separator.
5965
     *
5966
     * @param string $str       <p>The input string.</p>
5967
     * @param string $separator <p>The string separator.</p>
5968
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5969
     *
5970
     * @return string
5971
     */
5972
    public static function str_isubstr_before_first_separator(
5973
        string $str,
5974
        string $separator,
5975
        string $encoding = 'UTF-8'
5976
    ): string {
5977 1
        if ($separator === '' || $str === '') {
5978 1
            return '';
5979
        }
5980
5981 1
        $offset = self::stripos($str, $separator);
5982 1
        if ($offset === false) {
5983 1
            return '';
5984
        }
5985
5986 1
        if ($encoding === 'UTF-8') {
5987 1
            return (string) \mb_substr($str, 0, $offset);
5988
        }
5989
5990
        return (string) self::substr($str, 0, $offset, $encoding);
5991
    }
5992
5993
    /**
5994
     * Gets the substring before the last occurrence of a separator.
5995
     *
5996
     * @param string $str       <p>The input string.</p>
5997
     * @param string $separator <p>The string separator.</p>
5998
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5999
     *
6000
     * @return string
6001
     */
6002
    public static function str_isubstr_before_last_separator(
6003
        string $str,
6004
        string $separator,
6005
        string $encoding = 'UTF-8'
6006
    ): string {
6007 1
        if ($separator === '' || $str === '') {
6008 1
            return '';
6009
        }
6010
6011 1
        if ($encoding === 'UTF-8') {
6012 1
            $offset = \mb_strripos($str, $separator);
6013 1
            if ($offset === false) {
6014 1
                return '';
6015
            }
6016
6017 1
            return (string) \mb_substr($str, 0, $offset);
6018
        }
6019
6020
        $offset = self::strripos($str, $separator, 0, $encoding);
6021
        if ($offset === false) {
6022
            return '';
6023
        }
6024
6025
        return (string) self::substr($str, 0, $offset, $encoding);
6026
    }
6027
6028
    /**
6029
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
6030
     *
6031
     * @param string $str           <p>The input string.</p>
6032
     * @param string $needle        <p>The string to look for.</p>
6033
     * @param bool   $before_needle [optional] <p>Default: false</p>
6034
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6035
     *
6036
     * @return string
6037
     */
6038
    public static function str_isubstr_first(
6039
        string $str,
6040
        string $needle,
6041
        bool $before_needle = false,
6042
        string $encoding = 'UTF-8'
6043
    ): string {
6044
        if (
6045 2
            $needle === ''
6046
            ||
6047 2
            $str === ''
6048
        ) {
6049 2
            return '';
6050
        }
6051
6052 2
        $part = self::stristr(
6053 2
            $str,
6054 2
            $needle,
6055 2
            $before_needle,
6056 2
            $encoding
6057
        );
6058 2
        if ($part === false) {
6059 2
            return '';
6060
        }
6061
6062 2
        return $part;
6063
    }
6064
6065
    /**
6066
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
6067
     *
6068
     * @param string $str           <p>The input string.</p>
6069
     * @param string $needle        <p>The string to look for.</p>
6070
     * @param bool   $before_needle [optional] <p>Default: false</p>
6071
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6072
     *
6073
     * @return string
6074
     */
6075
    public static function str_isubstr_last(
6076
        string $str,
6077
        string $needle,
6078
        bool $before_needle = false,
6079
        string $encoding = 'UTF-8'
6080
    ): string {
6081
        if (
6082 1
            $needle === ''
6083
            ||
6084 1
            $str === ''
6085
        ) {
6086 1
            return '';
6087
        }
6088
6089 1
        $part = self::strrichr(
6090 1
            $str,
6091 1
            $needle,
6092 1
            $before_needle,
6093 1
            $encoding
6094
        );
6095 1
        if ($part === false) {
6096 1
            return '';
6097
        }
6098
6099 1
        return $part;
6100
    }
6101
6102
    /**
6103
     * Returns the last $n characters of the string.
6104
     *
6105
     * @param string $str      <p>The input string.</p>
6106
     * @param int    $n        <p>Number of characters to retrieve from the end.</p>
6107
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6108
     *
6109
     * @return string
6110
     */
6111
    public static function str_last_char(
6112
        string $str,
6113
        int $n = 1,
6114
        string $encoding = 'UTF-8'
6115
    ): string {
6116 12
        if ($str === '' || $n <= 0) {
6117 4
            return '';
6118
        }
6119
6120 8
        if ($encoding === 'UTF-8') {
6121 4
            return (string) \mb_substr($str, -$n);
6122
        }
6123
6124 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6125
6126 4
        return (string) self::substr($str, -$n, null, $encoding);
6127
    }
6128
6129
    /**
6130
     * Limit the number of characters in a string.
6131
     *
6132
     * @param string $str        <p>The input string.</p>
6133
     * @param int    $length     [optional] <p>Default: 100</p>
6134
     * @param string $str_add_on [optional] <p>Default: …</p>
6135
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6136
     *
6137
     * @return string
6138
     */
6139
    public static function str_limit(
6140
        string $str,
6141
        int $length = 100,
6142
        string $str_add_on = '…',
6143
        string $encoding = 'UTF-8'
6144
    ): string {
6145 2
        if ($str === '' || $length <= 0) {
6146 2
            return '';
6147
        }
6148
6149 2
        if ($encoding === 'UTF-8') {
6150 2
            if ((int) \mb_strlen($str) <= $length) {
6151 2
                return $str;
6152
            }
6153
6154
            /** @noinspection UnnecessaryCastingInspection */
6155 2
            return (string) \mb_substr($str, 0, $length - (int) self::strlen($str_add_on)) . $str_add_on;
6156
        }
6157
6158
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6159
6160
        if ((int) self::strlen($str, $encoding) <= $length) {
6161
            return $str;
6162
        }
6163
6164
        return ((string) self::substr($str, 0, $length - (int) self::strlen($str_add_on), $encoding)) . $str_add_on;
6165
    }
6166
6167
    /**
6168
     * Limit the number of characters in a string, but also after the next word.
6169
     *
6170
     * @param string $str        <p>The input string.</p>
6171
     * @param int    $length     [optional] <p>Default: 100</p>
6172
     * @param string $str_add_on [optional] <p>Default: …</p>
6173
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6174
     *
6175
     * @return string
6176
     */
6177
    public static function str_limit_after_word(
6178
        string $str,
6179
        int $length = 100,
6180
        string $str_add_on = '…',
6181
        string $encoding = 'UTF-8'
6182
    ): string {
6183 6
        if ($str === '' || $length <= 0) {
6184 2
            return '';
6185
        }
6186
6187 6
        if ($encoding === 'UTF-8') {
6188
            /** @noinspection UnnecessaryCastingInspection */
6189 2
            if ((int) \mb_strlen($str) <= $length) {
6190 2
                return $str;
6191
            }
6192
6193 2
            if (\mb_substr($str, $length - 1, 1) === ' ') {
6194 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6195
            }
6196
6197 2
            $str = \mb_substr($str, 0, $length);
6198
6199 2
            $array = \explode(' ', $str);
6200 2
            \array_pop($array);
6201 2
            $new_str = \implode(' ', $array);
6202
6203 2
            if ($new_str === '') {
6204 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6205
            }
6206
        } else {
6207 4
            if ((int) self::strlen($str, $encoding) <= $length) {
6208
                return $str;
6209
            }
6210
6211 4
            if (self::substr($str, $length - 1, 1, $encoding) === ' ') {
6212 3
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6213
            }
6214
6215
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6216 1
            $str = self::substr($str, 0, $length, $encoding);
6217
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6218 1
            if ($str === false) {
6219
                return '' . $str_add_on;
6220
            }
6221
6222 1
            $array = \explode(' ', $str);
6223 1
            \array_pop($array);
6224 1
            $new_str = \implode(' ', $array);
6225
6226 1
            if ($new_str === '') {
6227
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6228
            }
6229
        }
6230
6231 3
        return $new_str . $str_add_on;
6232
    }
6233
6234
    /**
6235
     * Returns the longest common prefix between the $str1 and $str2.
6236
     *
6237
     * @param string $str1     <p>The input sting.</p>
6238
     * @param string $str2     <p>Second string for comparison.</p>
6239
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6240
     *
6241
     * @return string
6242
     */
6243
    public static function str_longest_common_prefix(
6244
        string $str1,
6245
        string $str2,
6246
        string $encoding = 'UTF-8'
6247
    ): string {
6248
        // init
6249 10
        $longest_common_prefix = '';
6250
6251 10
        if ($encoding === 'UTF-8') {
6252 5
            $max_length = (int) \min(
6253 5
                \mb_strlen($str1),
6254 5
                \mb_strlen($str2)
6255
            );
6256
6257 5
            for ($i = 0; $i < $max_length; ++$i) {
6258 4
                $char = \mb_substr($str1, $i, 1);
6259
6260
                if (
6261 4
                    $char !== false
6262
                    &&
6263 4
                    $char === \mb_substr($str2, $i, 1)
6264
                ) {
6265 3
                    $longest_common_prefix .= $char;
6266
                } else {
6267 3
                    break;
6268
                }
6269
            }
6270
        } else {
6271 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6272
6273 5
            $max_length = (int) \min(
6274 5
                self::strlen($str1, $encoding),
6275 5
                self::strlen($str2, $encoding)
6276
            );
6277
6278 5
            for ($i = 0; $i < $max_length; ++$i) {
6279 4
                $char = self::substr($str1, $i, 1, $encoding);
6280
6281
                if (
6282 4
                    $char !== false
6283
                    &&
6284 4
                    $char === self::substr($str2, $i, 1, $encoding)
6285
                ) {
6286 3
                    $longest_common_prefix .= $char;
6287
                } else {
6288 3
                    break;
6289
                }
6290
            }
6291
        }
6292
6293 10
        return $longest_common_prefix;
6294
    }
6295
6296
    /**
6297
     * Returns the longest common substring between the $str1 and $str2.
6298
     * In the case of ties, it returns that which occurs first.
6299
     *
6300
     * @param string $str1
6301
     * @param string $str2     <p>Second string for comparison.</p>
6302
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6303
     *
6304
     * @return string string with its $str being the longest common substring
6305
     */
6306
    public static function str_longest_common_substring(
6307
        string $str1,
6308
        string $str2,
6309
        string $encoding = 'UTF-8'
6310
    ): string {
6311 11
        if ($str1 === '' || $str2 === '') {
6312 2
            return '';
6313
        }
6314
6315
        // Uses dynamic programming to solve
6316
        // http://en.wikipedia.org/wiki/Longest_common_substring_problem
6317
6318 9
        if ($encoding === 'UTF-8') {
6319 4
            $str_length = (int) \mb_strlen($str1);
6320 4
            $other_length = (int) \mb_strlen($str2);
6321
        } else {
6322 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6323
6324 5
            $str_length = (int) self::strlen($str1, $encoding);
6325 5
            $other_length = (int) self::strlen($str2, $encoding);
6326
        }
6327
6328
        // Return if either string is empty
6329 9
        if ($str_length === 0 || $other_length === 0) {
6330
            return '';
6331
        }
6332
6333 9
        $len = 0;
6334 9
        $end = 0;
6335 9
        $table = \array_fill(
6336 9
            0,
6337 9
            $str_length + 1,
6338 9
            \array_fill(0, $other_length + 1, 0)
6339
        );
6340
6341 9
        if ($encoding === 'UTF-8') {
6342 9
            for ($i = 1; $i <= $str_length; ++$i) {
6343 9
                for ($j = 1; $j <= $other_length; ++$j) {
6344 9
                    $str_char = \mb_substr($str1, $i - 1, 1);
6345 9
                    $other_char = \mb_substr($str2, $j - 1, 1);
6346
6347 9
                    if ($str_char === $other_char) {
6348 8
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6349 8
                        if ($table[$i][$j] > $len) {
6350 8
                            $len = $table[$i][$j];
6351 8
                            $end = $i;
6352
                        }
6353
                    } else {
6354 9
                        $table[$i][$j] = 0;
6355
                    }
6356
                }
6357
            }
6358
        } else {
6359
            for ($i = 1; $i <= $str_length; ++$i) {
6360
                for ($j = 1; $j <= $other_length; ++$j) {
6361
                    $str_char = self::substr($str1, $i - 1, 1, $encoding);
6362
                    $other_char = self::substr($str2, $j - 1, 1, $encoding);
6363
6364
                    if ($str_char === $other_char) {
6365
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6366
                        if ($table[$i][$j] > $len) {
6367
                            $len = $table[$i][$j];
6368
                            $end = $i;
6369
                        }
6370
                    } else {
6371
                        $table[$i][$j] = 0;
6372
                    }
6373
                }
6374
            }
6375
        }
6376
6377 9
        if ($encoding === 'UTF-8') {
6378 9
            return (string) \mb_substr($str1, $end - $len, $len);
6379
        }
6380
6381
        return (string) self::substr($str1, $end - $len, $len, $encoding);
6382
    }
6383
6384
    /**
6385
     * Returns the longest common suffix between the $str1 and $str2.
6386
     *
6387
     * @param string $str1
6388
     * @param string $str2     <p>Second string for comparison.</p>
6389
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6390
     *
6391
     * @return string
6392
     */
6393
    public static function str_longest_common_suffix(
6394
        string $str1,
6395
        string $str2,
6396
        string $encoding = 'UTF-8'
6397
    ): string {
6398 10
        if ($str1 === '' || $str2 === '') {
6399 2
            return '';
6400
        }
6401
6402 8
        if ($encoding === 'UTF-8') {
6403 4
            $max_length = (int) \min(
6404 4
                \mb_strlen($str1, $encoding),
6405 4
                \mb_strlen($str2, $encoding)
6406
            );
6407
6408 4
            $longest_common_suffix = '';
6409 4
            for ($i = 1; $i <= $max_length; ++$i) {
6410 4
                $char = \mb_substr($str1, -$i, 1);
6411
6412
                if (
6413 4
                    $char !== false
6414
                    &&
6415 4
                    $char === \mb_substr($str2, -$i, 1)
6416
                ) {
6417 3
                    $longest_common_suffix = $char . $longest_common_suffix;
6418
                } else {
6419 3
                    break;
6420
                }
6421
            }
6422
        } else {
6423 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6424
6425 4
            $max_length = (int) \min(
6426 4
                self::strlen($str1, $encoding),
6427 4
                self::strlen($str2, $encoding)
6428
            );
6429
6430 4
            $longest_common_suffix = '';
6431 4
            for ($i = 1; $i <= $max_length; ++$i) {
6432 4
                $char = self::substr($str1, -$i, 1, $encoding);
6433
6434
                if (
6435 4
                    $char !== false
6436
                    &&
6437 4
                    $char === self::substr($str2, -$i, 1, $encoding)
6438
                ) {
6439 3
                    $longest_common_suffix = $char . $longest_common_suffix;
6440
                } else {
6441 3
                    break;
6442
                }
6443
            }
6444
        }
6445
6446 8
        return $longest_common_suffix;
6447
    }
6448
6449
    /**
6450
     * Returns true if $str matches the supplied pattern, false otherwise.
6451
     *
6452
     * @param string $str     <p>The input string.</p>
6453
     * @param string $pattern <p>Regex pattern to match against.</p>
6454
     *
6455
     * @return bool whether or not $str matches the pattern
6456
     */
6457
    public static function str_matches_pattern(string $str, string $pattern): bool
6458
    {
6459
        return (bool) \preg_match('/' . $pattern . '/u', $str);
6460
    }
6461
6462
    /**
6463
     * Returns whether or not a character exists at an index. Offsets may be
6464
     * negative to count from the last character in the string. Implements
6465
     * part of the ArrayAccess interface.
6466
     *
6467
     * @param string $str      <p>The input string.</p>
6468
     * @param int    $offset   <p>The index to check.</p>
6469
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6470
     *
6471
     * @return bool whether or not the index exists
6472
     */
6473
    public static function str_offset_exists(string $str, int $offset, string $encoding = 'UTF-8'): bool
6474
    {
6475
        // init
6476 6
        $length = (int) self::strlen($str, $encoding);
6477
6478 6
        if ($offset >= 0) {
6479 3
            return $length > $offset;
6480
        }
6481
6482 3
        return $length >= \abs($offset);
6483
    }
6484
6485
    /**
6486
     * Returns the character at the given index. Offsets may be negative to
6487
     * count from the last character in the string. Implements part of the
6488
     * ArrayAccess interface, and throws an OutOfBoundsException if the index
6489
     * does not exist.
6490
     *
6491
     * @param string $str      <p>The input string.</p>
6492
     * @param int    $index    <p>The <strong>index</strong> from which to retrieve the char.</p>
6493
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6494
     *
6495
     * @throws \OutOfBoundsException if the positive or negative offset does not exist
6496
     *
6497
     * @return string the character at the specified index
6498
     */
6499
    public static function str_offset_get(string $str, int $index, string $encoding = 'UTF-8'): string
6500
    {
6501
        // init
6502 2
        $length = (int) self::strlen($str);
6503
6504
        if (
6505 2
            ($index >= 0 && $length <= $index)
6506
            ||
6507 2
            $length < \abs($index)
6508
        ) {
6509 1
            throw new \OutOfBoundsException('No character exists at the index');
6510
        }
6511
6512 1
        return self::char_at($str, $index, $encoding);
6513
    }
6514
6515
    /**
6516
     * Pad a UTF-8 string to a given length with another string.
6517
     *
6518
     * @param string     $str        <p>The input string.</p>
6519
     * @param int        $pad_length <p>The length of return string.</p>
6520
     * @param string     $pad_string [optional] <p>String to use for padding the input string.</p>
6521
     * @param int|string $pad_type   [optional] <p>
6522
     *                               Can be <strong>STR_PAD_RIGHT</strong> (default), [or string "right"]<br>
6523
     *                               <strong>STR_PAD_LEFT</strong> [or string "left"] or<br>
6524
     *                               <strong>STR_PAD_BOTH</strong> [or string "both"]
6525
     *                               </p>
6526
     * @param string     $encoding   [optional] <p>Default: 'UTF-8'</p>
6527
     *
6528
     * @return string returns the padded string
6529
     */
6530
    public static function str_pad(
6531
        string $str,
6532
        int $pad_length,
6533
        string $pad_string = ' ',
6534
        $pad_type = \STR_PAD_RIGHT,
6535
        string $encoding = 'UTF-8'
6536
    ): string {
6537 41
        if ($pad_length === 0 || $pad_string === '') {
6538 1
            return $str;
6539
        }
6540
6541 41
        if ($pad_type !== (int) $pad_type) {
6542 13
            if ($pad_type === 'left') {
6543 3
                $pad_type = \STR_PAD_LEFT;
6544 10
            } elseif ($pad_type === 'right') {
6545 6
                $pad_type = \STR_PAD_RIGHT;
6546 4
            } elseif ($pad_type === 'both') {
6547 3
                $pad_type = \STR_PAD_BOTH;
6548
            } else {
6549 1
                throw new \InvalidArgumentException(
6550 1
                    'Pad expects $pad_type to be "STR_PAD_*" or ' . "to be one of 'left', 'right' or 'both'"
6551
                );
6552
            }
6553
        }
6554
6555 40
        if ($encoding === 'UTF-8') {
6556 25
            $str_length = (int) \mb_strlen($str);
6557
6558 25
            if ($pad_length >= $str_length) {
6559
                switch ($pad_type) {
6560 25
                    case \STR_PAD_LEFT:
6561 8
                        $ps_length = (int) \mb_strlen($pad_string);
6562
6563 8
                        $diff = ($pad_length - $str_length);
6564
6565 8
                        $pre = (string) \mb_substr(
6566 8
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6567 8
                            0,
6568 8
                            $diff
6569
                        );
6570 8
                        $post = '';
6571
6572 8
                        break;
6573
6574 20
                    case \STR_PAD_BOTH:
6575 14
                        $diff = ($pad_length - $str_length);
6576
6577 14
                        $ps_length_left = (int) \floor($diff / 2);
6578
6579 14
                        $ps_length_right = (int) \ceil($diff / 2);
6580
6581 14
                        $pre = (string) \mb_substr(
6582 14
                            \str_repeat($pad_string, $ps_length_left),
6583 14
                            0,
6584 14
                            $ps_length_left
6585
                        );
6586 14
                        $post = (string) \mb_substr(
6587 14
                            \str_repeat($pad_string, $ps_length_right),
6588 14
                            0,
6589 14
                            $ps_length_right
6590
                        );
6591
6592 14
                        break;
6593
6594 9
                    case \STR_PAD_RIGHT:
6595
                    default:
6596 9
                        $ps_length = (int) \mb_strlen($pad_string);
6597
6598 9
                        $diff = ($pad_length - $str_length);
6599
6600 9
                        $post = (string) \mb_substr(
6601 9
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6602 9
                            0,
6603 9
                            $diff
6604
                        );
6605 9
                        $pre = '';
6606
                }
6607
6608 25
                return $pre . $str . $post;
6609
            }
6610
6611 3
            return $str;
6612
        }
6613
6614 15
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6615
6616 15
        $str_length = (int) self::strlen($str, $encoding);
6617
6618 15
        if ($pad_length >= $str_length) {
6619
            switch ($pad_type) {
6620 14
                case \STR_PAD_LEFT:
6621 5
                    $ps_length = (int) self::strlen($pad_string, $encoding);
6622
6623 5
                    $diff = ($pad_length - $str_length);
6624
6625 5
                    $pre = (string) self::substr(
6626 5
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6627 5
                        0,
6628 5
                        $diff,
6629 5
                        $encoding
6630
                    );
6631 5
                    $post = '';
6632
6633 5
                    break;
6634
6635 9
                case \STR_PAD_BOTH:
6636 3
                    $diff = ($pad_length - $str_length);
6637
6638 3
                    $ps_length_left = (int) \floor($diff / 2);
6639
6640 3
                    $ps_length_right = (int) \ceil($diff / 2);
6641
6642 3
                    $pre = (string) self::substr(
6643 3
                        \str_repeat($pad_string, $ps_length_left),
6644 3
                        0,
6645 3
                        $ps_length_left,
6646 3
                        $encoding
6647
                    );
6648 3
                    $post = (string) self::substr(
6649 3
                        \str_repeat($pad_string, $ps_length_right),
6650 3
                        0,
6651 3
                        $ps_length_right,
6652 3
                        $encoding
6653
                    );
6654
6655 3
                    break;
6656
6657 6
                case \STR_PAD_RIGHT:
6658
                default:
6659 6
                    $ps_length = (int) self::strlen($pad_string, $encoding);
6660
6661 6
                    $diff = ($pad_length - $str_length);
6662
6663 6
                    $post = (string) self::substr(
6664 6
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6665 6
                        0,
6666 6
                        $diff,
6667 6
                        $encoding
6668
                    );
6669 6
                    $pre = '';
6670
            }
6671
6672 14
            return $pre . $str . $post;
6673
        }
6674
6675 1
        return $str;
6676
    }
6677
6678
    /**
6679
     * Returns a new string of a given length such that both sides of the
6680
     * string are padded. Alias for "UTF8::str_pad()" with a $pad_type of 'both'.
6681
     *
6682
     * @param string $str
6683
     * @param int    $length   <p>Desired string length after padding.</p>
6684
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
6685
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6686
     *
6687
     * @return string
6688
     *                <p>The string with padding applied.</p>
6689
     */
6690
    public static function str_pad_both(
6691
        string $str,
6692
        int $length,
6693
        string $pad_str = ' ',
6694
        string $encoding = 'UTF-8'
6695
    ): string {
6696 11
        return self::str_pad(
6697 11
            $str,
6698 11
            $length,
6699 11
            $pad_str,
6700 11
            \STR_PAD_BOTH,
6701 11
            $encoding
6702
        );
6703
    }
6704
6705
    /**
6706
     * Returns a new string of a given length such that the beginning of the
6707
     * string is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'left'.
6708
     *
6709
     * @param string $str
6710
     * @param int    $length   <p>Desired string length after padding.</p>
6711
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
6712
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6713
     *
6714
     * @return string
6715
     *                <p>The string with left padding.</p>
6716
     */
6717
    public static function str_pad_left(
6718
        string $str,
6719
        int $length,
6720
        string $pad_str = ' ',
6721
        string $encoding = 'UTF-8'
6722
    ): string {
6723 7
        return self::str_pad(
6724 7
            $str,
6725 7
            $length,
6726 7
            $pad_str,
6727 7
            \STR_PAD_LEFT,
6728 7
            $encoding
6729
        );
6730
    }
6731
6732
    /**
6733
     * Returns a new string of a given length such that the end of the string
6734
     * is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'right'.
6735
     *
6736
     * @param string $str
6737
     * @param int    $length   <p>Desired string length after padding.</p>
6738
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
6739
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6740
     *
6741
     * @return string
6742
     *                <p>The string with right padding.</p>
6743
     */
6744
    public static function str_pad_right(
6745
        string $str,
6746
        int $length,
6747
        string $pad_str = ' ',
6748
        string $encoding = 'UTF-8'
6749
    ): string {
6750 7
        return self::str_pad(
6751 7
            $str,
6752 7
            $length,
6753 7
            $pad_str,
6754 7
            \STR_PAD_RIGHT,
6755 7
            $encoding
6756
        );
6757
    }
6758
6759
    /**
6760
     * Repeat a string.
6761
     *
6762
     * @param string $str        <p>
6763
     *                           The string to be repeated.
6764
     *                           </p>
6765
     * @param int    $multiplier <p>
6766
     *                           Number of time the input string should be
6767
     *                           repeated.
6768
     *                           </p>
6769
     *                           <p>
6770
     *                           multiplier has to be greater than or equal to 0.
6771
     *                           If the multiplier is set to 0, the function
6772
     *                           will return an empty string.
6773
     *                           </p>
6774
     *
6775
     * @return string
6776
     *                <p>The repeated string.</P>
6777
     */
6778
    public static function str_repeat(string $str, int $multiplier): string
6779
    {
6780 9
        $str = self::filter($str);
6781
6782 9
        return \str_repeat($str, $multiplier);
6783
    }
6784
6785
    /**
6786
     * INFO: This is only a wrapper for "str_replace()"  -> the original functions is already UTF-8 safe.
6787
     *
6788
     * Replace all occurrences of the search string with the replacement string
6789
     *
6790
     * @see http://php.net/manual/en/function.str-replace.php
6791
     *
6792
     * @param mixed $search  <p>
6793
     *                       The value being searched for, otherwise known as the needle.
6794
     *                       An array may be used to designate multiple needles.
6795
     *                       </p>
6796
     * @param mixed $replace <p>
6797
     *                       The replacement value that replaces found search
6798
     *                       values. An array may be used to designate multiple replacements.
6799
     *                       </p>
6800
     * @param mixed $subject <p>
6801
     *                       The string or array being searched and replaced on,
6802
     *                       otherwise known as the haystack.
6803
     *                       </p>
6804
     *                       <p>
6805
     *                       If subject is an array, then the search and
6806
     *                       replace is performed with every entry of
6807
     *                       subject, and the return value is an array as
6808
     *                       well.
6809
     *                       </p>
6810
     * @param int   $count   [optional] If passed, this will hold the number of matched and replaced needles
6811
     *
6812
     * @return mixed this function returns a string or an array with the replaced values
6813
     */
6814
    public static function str_replace(
6815
        $search,
6816
        $replace,
6817
        $subject,
6818
        int &$count = null
6819
    ) {
6820
        /**
6821
         * @psalm-suppress PossiblyNullArgument
6822
         */
6823 12
        return \str_replace(
6824 12
            $search,
6825 12
            $replace,
6826 12
            $subject,
6827 12
            $count
6828
        );
6829
    }
6830
6831
    /**
6832
     * Replaces $search from the beginning of string with $replacement.
6833
     *
6834
     * @param string $str         <p>The input string.</p>
6835
     * @param string $search      <p>The string to search for.</p>
6836
     * @param string $replacement <p>The replacement.</p>
6837
     *
6838
     * @return string string after the replacements
6839
     */
6840
    public static function str_replace_beginning(
6841
        string $str,
6842
        string $search,
6843
        string $replacement
6844
    ): string {
6845 17
        if ($str === '') {
6846 4
            if ($replacement === '') {
6847 2
                return '';
6848
            }
6849
6850 2
            if ($search === '') {
6851 2
                return $replacement;
6852
            }
6853
        }
6854
6855 13
        if ($search === '') {
6856 2
            return $str . $replacement;
6857
        }
6858
6859 11
        if (\strpos($str, $search) === 0) {
6860 9
            return $replacement . \substr($str, \strlen($search));
6861
        }
6862
6863 2
        return $str;
6864
    }
6865
6866
    /**
6867
     * Replaces $search from the ending of string with $replacement.
6868
     *
6869
     * @param string $str         <p>The input string.</p>
6870
     * @param string $search      <p>The string to search for.</p>
6871
     * @param string $replacement <p>The replacement.</p>
6872
     *
6873
     * @return string string after the replacements
6874
     */
6875
    public static function str_replace_ending(
6876
        string $str,
6877
        string $search,
6878
        string $replacement
6879
    ): string {
6880 17
        if ($str === '') {
6881 4
            if ($replacement === '') {
6882 2
                return '';
6883
            }
6884
6885 2
            if ($search === '') {
6886 2
                return $replacement;
6887
            }
6888
        }
6889
6890 13
        if ($search === '') {
6891 2
            return $str . $replacement;
6892
        }
6893
6894 11
        if (\strpos($str, $search, \strlen($str) - \strlen($search)) !== false) {
6895 8
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
6896
        }
6897
6898 11
        return $str;
6899
    }
6900
6901
    /**
6902
     * Replace the first "$search"-term with the "$replace"-term.
6903
     *
6904
     * @param string $search
6905
     * @param string $replace
6906
     * @param string $subject
6907
     *
6908
     * @return string
6909
     *
6910
     * @psalm-suppress InvalidReturnType
6911
     */
6912
    public static function str_replace_first(
6913
        string $search,
6914
        string $replace,
6915
        string $subject
6916
    ): string {
6917 2
        $pos = self::strpos($subject, $search);
6918
6919 2
        if ($pos !== false) {
6920
            /**
6921
             * @psalm-suppress InvalidReturnStatement
6922
             */
6923 2
            return self::substr_replace(
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::substr_repl...)self::strlen($search)) could return the type string[] which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
6924 2
                $subject,
6925 2
                $replace,
6926 2
                $pos,
6927 2
                (int) self::strlen($search)
6928
            );
6929
        }
6930
6931 2
        return $subject;
6932
    }
6933
6934
    /**
6935
     * Replace the last "$search"-term with the "$replace"-term.
6936
     *
6937
     * @param string $search
6938
     * @param string $replace
6939
     * @param string $subject
6940
     *
6941
     * @return string
6942
     *
6943
     * @psalm-suppress InvalidReturnType
6944
     */
6945
    public static function str_replace_last(
6946
        string $search,
6947
        string $replace,
6948
        string $subject
6949
    ): string {
6950 2
        $pos = self::strrpos($subject, $search);
6951 2
        if ($pos !== false) {
6952
            /**
6953
             * @psalm-suppress InvalidReturnStatement
6954
             */
6955 2
            return self::substr_replace(
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::substr_repl...)self::strlen($search)) could return the type string[] which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
6956 2
                $subject,
6957 2
                $replace,
6958 2
                $pos,
6959 2
                (int) self::strlen($search)
6960
            );
6961
        }
6962
6963 2
        return $subject;
6964
    }
6965
6966
    /**
6967
     * Shuffles all the characters in the string.
6968
     *
6969
     * PS: uses random algorithm which is weak for cryptography purposes
6970
     *
6971
     * @param string $str      <p>The input string</p>
6972
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6973
     *
6974
     * @return string the shuffled string
6975
     */
6976
    public static function str_shuffle(string $str, string $encoding = 'UTF-8'): string
6977
    {
6978 5
        if ($encoding === 'UTF-8') {
6979 5
            $indexes = \range(0, (int) \mb_strlen($str) - 1);
6980
            /** @noinspection NonSecureShuffleUsageInspection */
6981 5
            \shuffle($indexes);
6982
6983
            // init
6984 5
            $shuffled_str = '';
6985
6986 5
            foreach ($indexes as &$i) {
6987 5
                $tmp_sub_str = \mb_substr($str, $i, 1);
6988 5
                if ($tmp_sub_str !== false) {
6989 5
                    $shuffled_str .= $tmp_sub_str;
6990
                }
6991
            }
6992
        } else {
6993
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6994
6995
            $indexes = \range(0, (int) self::strlen($str, $encoding) - 1);
6996
            /** @noinspection NonSecureShuffleUsageInspection */
6997
            \shuffle($indexes);
6998
6999
            // init
7000
            $shuffled_str = '';
7001
7002
            foreach ($indexes as &$i) {
7003
                $tmp_sub_str = self::substr($str, $i, 1, $encoding);
7004
                if ($tmp_sub_str !== false) {
7005
                    $shuffled_str .= $tmp_sub_str;
7006
                }
7007
            }
7008
        }
7009
7010 5
        return $shuffled_str;
7011
    }
7012
7013
    /**
7014
     * Returns the substring beginning at $start, and up to, but not including
7015
     * the index specified by $end. If $end is omitted, the function extracts
7016
     * the remaining string. If $end is negative, it is computed from the end
7017
     * of the string.
7018
     *
7019
     * @param string $str
7020
     * @param int    $start    <p>Initial index from which to begin extraction.</p>
7021
     * @param int    $end      [optional] <p>Index at which to end extraction. Default: null</p>
7022
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7023
     *
7024
     * @return false|string
7025
     *                      <p>The extracted substring.</p><p>If <i>str</i> is shorter than <i>start</i>
7026
     *                      characters long, <b>FALSE</b> will be returned.
7027
     */
7028
    public static function str_slice(
7029
        string $str,
7030
        int $start,
7031
        int $end = null,
7032
        string $encoding = 'UTF-8'
7033
    ) {
7034 18
        if ($encoding === 'UTF-8') {
7035 7
            if ($end === null) {
7036 1
                $length = (int) \mb_strlen($str);
7037 6
            } elseif ($end >= 0 && $end <= $start) {
7038 2
                return '';
7039 4
            } elseif ($end < 0) {
7040 1
                $length = (int) \mb_strlen($str) + $end - $start;
7041
            } else {
7042 3
                $length = $end - $start;
7043
            }
7044
7045 5
            return \mb_substr($str, $start, $length);
7046
        }
7047
7048 11
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7049
7050 11
        if ($end === null) {
7051 5
            $length = (int) self::strlen($str, $encoding);
7052 6
        } elseif ($end >= 0 && $end <= $start) {
7053 2
            return '';
7054 4
        } elseif ($end < 0) {
7055 1
            $length = (int) self::strlen($str, $encoding) + $end - $start;
7056
        } else {
7057 3
            $length = $end - $start;
7058
        }
7059
7060 9
        return self::substr($str, $start, $length, $encoding);
7061
    }
7062
7063
    /**
7064
     * Convert a string to e.g.: "snake_case"
7065
     *
7066
     * @param string $str
7067
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7068
     *
7069
     * @return string string in snake_case
7070
     */
7071
    public static function str_snakeize(string $str, string $encoding = 'UTF-8'): string
7072
    {
7073 22
        if ($str === '') {
7074
            return '';
7075
        }
7076
7077 22
        $str = \str_replace(
7078 22
            '-',
7079 22
            '_',
7080 22
            self::normalize_whitespace($str)
7081
        );
7082
7083 22
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
7084 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7085
        }
7086
7087 22
        $str = (string) \preg_replace_callback(
7088 22
            '/([\\p{N}|\\p{Lu}])/u',
7089
            /**
7090
             * @param string[] $matches
7091
             *
7092
             * @return string
7093
             */
7094
            static function (array $matches) use ($encoding): string {
7095 9
                $match = $matches[1];
7096 9
                $match_int = (int) $match;
7097
7098 9
                if ((string) $match_int === $match) {
7099 4
                    return '_' . $match . '_';
7100
                }
7101
7102 5
                if ($encoding === 'UTF-8') {
7103 5
                    return '_' . \mb_strtolower($match);
7104
                }
7105
7106
                return '_' . self::strtolower($match, $encoding);
7107 22
            },
7108 22
            $str
7109
        );
7110
7111 22
        $str = (string) \preg_replace(
7112
            [
7113 22
                '/\\s+/u',           // convert spaces to "_"
7114
                '/^\\s+|\\s+$/u', // trim leading & trailing spaces
7115
                '/_+/',                 // remove double "_"
7116
            ],
7117
            [
7118 22
                '_',
7119
                '',
7120
                '_',
7121
            ],
7122 22
            $str
7123
        );
7124
7125 22
        return \trim(\trim($str, '_')); // trim leading & trailing "_" + whitespace
7126
    }
7127
7128
    /**
7129
     * Sort all characters according to code points.
7130
     *
7131
     * @param string $str    <p>A UTF-8 string.</p>
7132
     * @param bool   $unique <p>Sort unique. If <strong>true</strong>, repeated characters are ignored.</p>
7133
     * @param bool   $desc   <p>If <strong>true</strong>, will sort characters in reverse code point order.</p>
7134
     *
7135
     * @return string string of sorted characters
7136
     */
7137
    public static function str_sort(string $str, bool $unique = false, bool $desc = false): string
7138
    {
7139 2
        $array = self::codepoints($str);
7140
7141 2
        if ($unique) {
7142 2
            $array = \array_flip(\array_flip($array));
7143
        }
7144
7145 2
        if ($desc) {
7146 2
            \arsort($array);
0 ignored issues
show
Bug introduced by
It seems like $array can also be of type null; however, parameter $array of arsort() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7146
            \arsort(/** @scrutinizer ignore-type */ $array);
Loading history...
7147
        } else {
7148 2
            \asort($array);
0 ignored issues
show
Bug introduced by
It seems like $array can also be of type null; however, parameter $array of asort() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

7148
            \asort(/** @scrutinizer ignore-type */ $array);
Loading history...
7149
        }
7150
7151 2
        return self::string($array);
7152
    }
7153
7154
    /**
7155
     * Convert a string to an array of Unicode characters.
7156
     *
7157
     * @param int|int[]|string|string[] $str                     <p>The string to split into array.</p>
7158
     * @param int                       $length                  [optional] <p>Max character length of each array
7159
     *                                                           element.</p>
7160
     * @param bool                      $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
7161
     * @param bool                      $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
7162
     *                                                           "mb_substr"</p>
7163
     *
7164
     * @return array
7165
     *               <p>An array containing chunks of the input.</p>
7166
     */
7167
    public static function str_split(
7168
        $str,
7169
        int $length = 1,
7170
        bool $clean_utf8 = false,
7171
        bool $try_to_use_mb_functions = true
7172
    ): array {
7173 89
        if ($length <= 0) {
7174 3
            return [];
7175
        }
7176
7177 88
        if (\is_array($str) === true) {
7178 2
            foreach ($str as $k => &$v) {
7179 2
                $v = self::str_split(
7180 2
                    $v,
7181 2
                    $length,
7182 2
                    $clean_utf8,
7183 2
                    $try_to_use_mb_functions
7184
                );
7185
            }
7186
7187 2
            return $str;
7188
        }
7189
7190
        // init
7191 88
        $str = (string) $str;
7192
7193 88
        if ($str === '') {
7194 13
            return [];
7195
        }
7196
7197 85
        if ($clean_utf8 === true) {
7198 19
            $str = self::clean($str);
7199
        }
7200
7201
        if (
7202 85
            $try_to_use_mb_functions === true
7203
            &&
7204 85
            self::$SUPPORT['mbstring'] === true
7205
        ) {
7206 81
            $i_max = \mb_strlen($str);
7207 81
            if ($i_max <= 127) {
7208 75
                $ret = [];
7209 75
                for ($i = 0; $i < $i_max; ++$i) {
7210 75
                    $ret[] = \mb_substr($str, $i, 1);
7211
                }
7212
            } else {
7213 16
                $return_array = [];
7214 16
                \preg_match_all('/./us', $str, $return_array);
7215 81
                $ret = $return_array[0] ?? [];
7216
            }
7217 23
        } elseif (self::$SUPPORT['pcre_utf8'] === true) {
7218 17
            $return_array = [];
7219 17
            \preg_match_all('/./us', $str, $return_array);
7220 17
            $ret = $return_array[0] ?? [];
7221
        } else {
7222
7223
            // fallback
7224
7225 8
            $ret = [];
7226 8
            $len = \strlen($str);
7227
7228
            /** @noinspection ForeachInvariantsInspection */
7229 8
            for ($i = 0; $i < $len; ++$i) {
7230 8
                if (($str[$i] & "\x80") === "\x00") {
7231 8
                    $ret[] = $str[$i];
7232
                } elseif (
7233 8
                    isset($str[$i + 1])
7234
                    &&
7235 8
                    ($str[$i] & "\xE0") === "\xC0"
7236
                ) {
7237 4
                    if (($str[$i + 1] & "\xC0") === "\x80") {
7238 4
                        $ret[] = $str[$i] . $str[$i + 1];
7239
7240 4
                        ++$i;
7241
                    }
7242
                } elseif (
7243 6
                    isset($str[$i + 2])
7244
                    &&
7245 6
                    ($str[$i] & "\xF0") === "\xE0"
7246
                ) {
7247
                    if (
7248 6
                        ($str[$i + 1] & "\xC0") === "\x80"
7249
                        &&
7250 6
                        ($str[$i + 2] & "\xC0") === "\x80"
7251
                    ) {
7252 6
                        $ret[] = $str[$i] . $str[$i + 1] . $str[$i + 2];
7253
7254 6
                        $i += 2;
7255
                    }
7256
                } elseif (
7257
                    isset($str[$i + 3])
7258
                    &&
7259
                    ($str[$i] & "\xF8") === "\xF0"
7260
                ) {
7261
                    if (
7262
                        ($str[$i + 1] & "\xC0") === "\x80"
7263
                        &&
7264
                        ($str[$i + 2] & "\xC0") === "\x80"
7265
                        &&
7266
                        ($str[$i + 3] & "\xC0") === "\x80"
7267
                    ) {
7268
                        $ret[] = $str[$i] . $str[$i + 1] . $str[$i + 2] . $str[$i + 3];
7269
7270
                        $i += 3;
7271
                    }
7272
                }
7273
            }
7274
        }
7275
7276 85
        if ($length > 1) {
7277 11
            $ret = \array_chunk($ret, $length);
7278
7279 11
            return \array_map(
7280
                static function (array &$item): string {
7281 11
                    return \implode('', $item);
7282 11
                },
7283 11
                $ret
7284
            );
7285
        }
7286
7287 78
        if (isset($ret[0]) && $ret[0] === '') {
7288
            return [];
7289
        }
7290
7291 78
        return $ret;
7292
    }
7293
7294
    /**
7295
     * Splits the string with the provided regular expression, returning an
7296
     * array of strings. An optional integer $limit will truncate the
7297
     * results.
7298
     *
7299
     * @param string $str
7300
     * @param string $pattern <p>The regex with which to split the string.</p>
7301
     * @param int    $limit   [optional] <p>Maximum number of results to return. Default: -1 === no limit</p>
7302
     *
7303
     * @return string[] an array of strings
7304
     */
7305
    public static function str_split_pattern(string $str, string $pattern, int $limit = -1): array
7306
    {
7307 16
        if ($limit === 0) {
7308 2
            return [];
7309
        }
7310
7311 14
        if ($pattern === '') {
7312 1
            return [$str];
7313
        }
7314
7315 13
        if (self::$SUPPORT['mbstring'] === true) {
7316 13
            if ($limit >= 0) {
7317
                /** @noinspection PhpComposerExtensionStubsInspection */
7318 8
                $result_tmp = \mb_split($pattern, $str);
7319
7320 8
                $result = [];
7321 8
                foreach ($result_tmp as $item_tmp) {
7322 8
                    if ($limit === 0) {
7323 4
                        break;
7324
                    }
7325 8
                    --$limit;
7326
7327 8
                    $result[] = $item_tmp;
7328
                }
7329
7330 8
                return $result;
7331
            }
7332
7333
            /** @noinspection PhpComposerExtensionStubsInspection */
7334 5
            return \mb_split($pattern, $str);
7335
        }
7336
7337
        if ($limit > 0) {
7338
            ++$limit;
7339
        } else {
7340
            $limit = -1;
7341
        }
7342
7343
        $array = \preg_split('/' . \preg_quote($pattern, '/') . '/u', $str, $limit);
7344
7345
        if ($array === false) {
7346
            return [];
7347
        }
7348
7349
        if ($limit > 0 && \count($array) === $limit) {
7350
            \array_pop($array);
7351
        }
7352
7353
        return $array;
7354
    }
7355
7356
    /**
7357
     * Check if the string starts with the given substring.
7358
     *
7359
     * @param string $haystack <p>The string to search in.</p>
7360
     * @param string $needle   <p>The substring to search for.</p>
7361
     *
7362
     * @return bool
7363
     */
7364
    public static function str_starts_with(string $haystack, string $needle): bool
7365
    {
7366 19
        if ($needle === '') {
7367 2
            return true;
7368
        }
7369
7370 19
        if ($haystack === '') {
7371
            return false;
7372
        }
7373
7374 19
        return \strpos($haystack, $needle) === 0;
7375
    }
7376
7377
    /**
7378
     * Returns true if the string begins with any of $substrings, false otherwise.
7379
     *
7380
     * - case-sensitive
7381
     *
7382
     * @param string $str        <p>The input string.</p>
7383
     * @param array  $substrings <p>Substrings to look for.</p>
7384
     *
7385
     * @return bool whether or not $str starts with $substring
7386
     */
7387
    public static function str_starts_with_any(string $str, array $substrings): bool
7388
    {
7389 8
        if ($str === '') {
7390
            return false;
7391
        }
7392
7393 8
        if ($substrings === []) {
7394
            return false;
7395
        }
7396
7397 8
        foreach ($substrings as &$substring) {
7398 8
            if (self::str_starts_with($str, $substring)) {
7399 8
                return true;
7400
            }
7401
        }
7402
7403 6
        return false;
7404
    }
7405
7406
    /**
7407
     * Gets the substring after the first occurrence of a separator.
7408
     *
7409
     * @param string $str       <p>The input string.</p>
7410
     * @param string $separator <p>The string separator.</p>
7411
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7412
     *
7413
     * @return string
7414
     */
7415
    public static function str_substr_after_first_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
7416
    {
7417 1
        if ($separator === '' || $str === '') {
7418 1
            return '';
7419
        }
7420
7421 1
        if ($encoding === 'UTF-8') {
7422 1
            $offset = \mb_strpos($str, $separator);
7423 1
            if ($offset === false) {
7424 1
                return '';
7425
            }
7426
7427 1
            return (string) \mb_substr(
7428 1
                $str,
7429 1
                $offset + (int) \mb_strlen($separator)
7430
            );
7431
        }
7432
7433
        $offset = self::strpos($str, $separator, 0, $encoding);
7434
        if ($offset === false) {
7435
            return '';
7436
        }
7437
7438
        return (string) \mb_substr(
7439
            $str,
7440
            $offset + (int) self::strlen($separator, $encoding),
7441
            null,
7442
            $encoding
7443
        );
7444
    }
7445
7446
    /**
7447
     * Gets the substring after the last occurrence of a separator.
7448
     *
7449
     * @param string $str       <p>The input string.</p>
7450
     * @param string $separator <p>The string separator.</p>
7451
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7452
     *
7453
     * @return string
7454
     */
7455
    public static function str_substr_after_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
7456
    {
7457 1
        if ($separator === '' || $str === '') {
7458 1
            return '';
7459
        }
7460
7461 1
        if ($encoding === 'UTF-8') {
7462 1
            $offset = \mb_strrpos($str, $separator);
7463 1
            if ($offset === false) {
7464 1
                return '';
7465
            }
7466
7467 1
            return (string) \mb_substr(
7468 1
                $str,
7469 1
                $offset + (int) \mb_strlen($separator)
7470
            );
7471
        }
7472
7473
        $offset = self::strrpos($str, $separator, 0, $encoding);
7474
        if ($offset === false) {
7475
            return '';
7476
        }
7477
7478
        return (string) self::substr(
7479
            $str,
7480
            $offset + (int) self::strlen($separator, $encoding),
7481
            null,
7482
            $encoding
7483
        );
7484
    }
7485
7486
    /**
7487
     * Gets the substring before the first occurrence of a separator.
7488
     *
7489
     * @param string $str       <p>The input string.</p>
7490
     * @param string $separator <p>The string separator.</p>
7491
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7492
     *
7493
     * @return string
7494
     */
7495
    public static function str_substr_before_first_separator(
7496
        string $str,
7497
        string $separator,
7498
        string $encoding = 'UTF-8'
7499
    ): string {
7500 1
        if ($separator === '' || $str === '') {
7501 1
            return '';
7502
        }
7503
7504 1
        if ($encoding === 'UTF-8') {
7505 1
            $offset = \mb_strpos($str, $separator);
7506 1
            if ($offset === false) {
7507 1
                return '';
7508
            }
7509
7510 1
            return (string) \mb_substr(
7511 1
                $str,
7512 1
                0,
7513 1
                $offset
7514
            );
7515
        }
7516
7517
        $offset = self::strpos($str, $separator, 0, $encoding);
7518
        if ($offset === false) {
7519
            return '';
7520
        }
7521
7522
        return (string) self::substr(
7523
            $str,
7524
            0,
7525
            $offset,
7526
            $encoding
7527
        );
7528
    }
7529
7530
    /**
7531
     * Gets the substring before the last occurrence of a separator.
7532
     *
7533
     * @param string $str       <p>The input string.</p>
7534
     * @param string $separator <p>The string separator.</p>
7535
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7536
     *
7537
     * @return string
7538
     */
7539
    public static function str_substr_before_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
7540
    {
7541 1
        if ($separator === '' || $str === '') {
7542 1
            return '';
7543
        }
7544
7545 1
        if ($encoding === 'UTF-8') {
7546 1
            $offset = \mb_strrpos($str, $separator);
7547 1
            if ($offset === false) {
7548 1
                return '';
7549
            }
7550
7551 1
            return (string) \mb_substr(
7552 1
                $str,
7553 1
                0,
7554 1
                $offset
7555
            );
7556
        }
7557
7558
        $offset = self::strrpos($str, $separator, 0, $encoding);
7559
        if ($offset === false) {
7560
            return '';
7561
        }
7562
7563
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7564
7565
        return (string) self::substr(
7566
            $str,
7567
            0,
7568
            $offset,
7569
            $encoding
7570
        );
7571
    }
7572
7573
    /**
7574
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
7575
     *
7576
     * @param string $str           <p>The input string.</p>
7577
     * @param string $needle        <p>The string to look for.</p>
7578
     * @param bool   $before_needle [optional] <p>Default: false</p>
7579
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
7580
     *
7581
     * @return string
7582
     */
7583
    public static function str_substr_first(
7584
        string $str,
7585
        string $needle,
7586
        bool $before_needle = false,
7587
        string $encoding = 'UTF-8'
7588
    ): string {
7589 2
        if ($str === '' || $needle === '') {
7590 2
            return '';
7591
        }
7592
7593 2
        if ($encoding === 'UTF-8') {
7594 2
            if ($before_needle === true) {
7595 1
                $part = \mb_strstr(
7596 1
                    $str,
7597 1
                    $needle,
7598 1
                    $before_needle
7599
                );
7600
            } else {
7601 1
                $part = \mb_strstr(
7602 1
                    $str,
7603 2
                    $needle
7604
                );
7605
            }
7606
        } else {
7607
            $part = self::strstr(
7608
                $str,
7609
                $needle,
7610
                $before_needle,
7611
                $encoding
7612
            );
7613
        }
7614
7615 2
        return $part === false ? '' : $part;
7616
    }
7617
7618
    /**
7619
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
7620
     *
7621
     * @param string $str           <p>The input string.</p>
7622
     * @param string $needle        <p>The string to look for.</p>
7623
     * @param bool   $before_needle [optional] <p>Default: false</p>
7624
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
7625
     *
7626
     * @return string
7627
     */
7628
    public static function str_substr_last(
7629
        string $str,
7630
        string $needle,
7631
        bool $before_needle = false,
7632
        string $encoding = 'UTF-8'
7633
    ): string {
7634 2
        if ($str === '' || $needle === '') {
7635 2
            return '';
7636
        }
7637
7638 2
        if ($encoding === 'UTF-8') {
7639 2
            if ($before_needle === true) {
7640 1
                $part = \mb_strrchr(
7641 1
                    $str,
7642 1
                    $needle,
7643 1
                    $before_needle
7644
                );
7645
            } else {
7646 1
                $part = \mb_strrchr(
7647 1
                    $str,
7648 2
                    $needle
7649
                );
7650
            }
7651
        } else {
7652
            $part = self::strrchr(
7653
                $str,
7654
                $needle,
7655
                $before_needle,
7656
                $encoding
7657
            );
7658
        }
7659
7660 2
        return $part === false ? '' : $part;
7661
    }
7662
7663
    /**
7664
     * Surrounds $str with the given substring.
7665
     *
7666
     * @param string $str
7667
     * @param string $substring <p>The substring to add to both sides.</P>
7668
     *
7669
     * @return string string with the substring both prepended and appended
7670
     */
7671
    public static function str_surround(string $str, string $substring): string
7672
    {
7673 5
        return $substring . $str . $substring;
7674
    }
7675
7676
    /**
7677
     * Returns a trimmed string with the first letter of each word capitalized.
7678
     * Also accepts an array, $ignore, allowing you to list words not to be
7679
     * capitalized.
7680
     *
7681
     * @param string              $str
7682
     * @param array|string[]|null $ignore                        [optional] <p>An array of words not to capitalize or null.
7683
     *                                                           Default: null</p>
7684
     * @param string              $encoding                      [optional] <p>Default: 'UTF-8'</p>
7685
     * @param bool                $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
7686
     * @param string|null         $lang                          [optional] <p>Set the language for special cases: az, el, lt,
7687
     *                                                           tr</p>
7688
     * @param bool                $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
7689
     *                                                           ß</p>
7690
     * @param bool                $use_trim_first                [optional] <p>true === trim the input string, first</p>
7691
     * @param string|null         $word_define_chars             [optional] <p>An string of chars that will be used as whitespace separator === words.</p>
7692
     *
7693
     * @return string
7694
     *                <p>The titleized string.</p>
7695
     */
7696
    public static function str_titleize(
7697
        string $str,
7698
        array $ignore = null,
7699
        string $encoding = 'UTF-8',
7700
        bool $clean_utf8 = false,
7701
        string $lang = null,
7702
        bool $try_to_keep_the_string_length = false,
7703
        bool $use_trim_first = true,
7704
        string $word_define_chars = null
7705
    ): string {
7706 10
        if ($str === '') {
7707
            return '';
7708
        }
7709
7710 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
7711 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7712
        }
7713
7714 10
        if ($use_trim_first === true) {
7715 10
            $str = \trim($str);
7716
        }
7717
7718 10
        if ($clean_utf8 === true) {
7719
            $str = self::clean($str);
7720
        }
7721
7722 10
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
7723
7724 10
        if ($word_define_chars) {
7725 4
            $word_define_chars = \preg_quote($word_define_chars, '/');
7726
        } else {
7727 6
            $word_define_chars = '';
7728
        }
7729
7730 10
        $str = (string) \preg_replace_callback(
7731 10
            '/([^\\s' . $word_define_chars . ']+)/u',
7732
            static function (array $match) use ($try_to_keep_the_string_length, $lang, $ignore, $use_mb_functions, $encoding): string {
7733 10
                if ($ignore !== null && \in_array($match[0], $ignore, true)) {
7734 4
                    return $match[0];
7735
                }
7736
7737 10
                if ($use_mb_functions === true) {
7738 10
                    if ($encoding === 'UTF-8') {
7739 10
                        return \mb_strtoupper(\mb_substr($match[0], 0, 1))
7740 10
                               . \mb_strtolower(\mb_substr($match[0], 1));
7741
                    }
7742
7743
                    return \mb_strtoupper(\mb_substr($match[0], 0, 1, $encoding), $encoding)
7744
                           . \mb_strtolower(\mb_substr($match[0], 1, null, $encoding), $encoding);
7745
                }
7746
7747
                return self::ucfirst(
7748
                    self::strtolower(
7749
                        $match[0],
7750
                        $encoding,
7751
                        false,
7752
                        $lang,
7753
                        $try_to_keep_the_string_length
7754
                    ),
7755
                    $encoding,
7756
                    false,
7757
                    $lang,
7758
                    $try_to_keep_the_string_length
7759
                );
7760 10
            },
7761 10
            $str
7762
        );
7763
7764 10
        return $str;
7765
    }
7766
7767
    /**
7768
     * Returns a trimmed string in proper title case.
7769
     *
7770
     * Also accepts an array, $ignore, allowing you to list words not to be
7771
     * capitalized.
7772
     *
7773
     * Adapted from John Gruber's script.
7774
     *
7775
     * @see https://gist.github.com/gruber/9f9e8650d68b13ce4d78
7776
     *
7777
     * @param string $str
7778
     * @param array  $ignore   <p>An array of words not to capitalize.</p>
7779
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7780
     *
7781
     * @return string the titleized string
7782
     */
7783
    public static function str_titleize_for_humans(
7784
        string $str,
7785
        array $ignore = [],
7786
        string $encoding = 'UTF-8'
7787
    ): string {
7788 35
        $small_words = \array_merge(
7789
            [
7790 35
                '(?<!q&)a',
7791
                'an',
7792
                'and',
7793
                'as',
7794
                'at(?!&t)',
7795
                'but',
7796
                'by',
7797
                'en',
7798
                'for',
7799
                'if',
7800
                'in',
7801
                'of',
7802
                'on',
7803
                'or',
7804
                'the',
7805
                'to',
7806
                'v[.]?',
7807
                'via',
7808
                'vs[.]?',
7809
            ],
7810 35
            $ignore
7811
        );
7812
7813 35
        $small_words_rx = \implode('|', $small_words);
7814 35
        $apostrophe_rx = '(?x: [\'’] [[:lower:]]* )?';
7815
7816 35
        $str = \trim($str);
7817
7818 35
        if (self::has_lowercase($str) === false) {
7819 2
            $str = self::strtolower($str, $encoding);
7820
        }
7821
7822
        // the main substitutions
7823 35
        $str = (string) \preg_replace_callback(
7824
            '~\\b (_*) (?:                                                         # 1. Leading underscore and
7825
                        ( (?<=[ ][/\\\\]) [[:alpha:]]+ [-_[:alpha:]/\\\\]+ |              # 2. file path or 
7826 35
                          [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ' . $apostrophe_rx . ' ) #    URL, domain, or email
7827
                        |
7828 35
                        ( (?i: ' . $small_words_rx . ' ) ' . $apostrophe_rx . ' )            # 3. or small word (case-insensitive)
7829
                        |
7830 35
                        ( [[:alpha:]] [[:lower:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 4. or word w/o internal caps
7831
                        |
7832 35
                        ( [[:alpha:]] [[:alpha:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 5. or some other word
7833
                      ) (_*) \\b                                                          # 6. With trailing underscore
7834
                    ~ux',
7835
            /**
7836
             * @param string[] $matches
7837
             *
7838
             * @return string
7839
             */
7840
            static function (array $matches) use ($encoding): string {
7841
                // preserve leading underscore
7842 35
                $str = $matches[1];
7843 35
                if ($matches[2]) {
7844
                    // preserve URLs, domains, emails and file paths
7845 5
                    $str .= $matches[2];
7846 35
                } elseif ($matches[3]) {
7847
                    // lower-case small words
7848 25
                    $str .= self::strtolower($matches[3], $encoding);
7849 35
                } elseif ($matches[4]) {
7850
                    // capitalize word w/o internal caps
7851 34
                    $str .= static::ucfirst($matches[4], $encoding);
7852
                } else {
7853
                    // preserve other kinds of word (iPhone)
7854 7
                    $str .= $matches[5];
7855
                }
7856
                // preserve trailing underscore
7857 35
                $str .= $matches[6];
7858
7859 35
                return $str;
7860 35
            },
7861 35
            $str
7862
        );
7863
7864
        // Exceptions for small words: capitalize at start of title...
7865 35
        $str = (string) \preg_replace_callback(
7866
            '~(  \\A [[:punct:]]*            # start of title...
7867
                      |  [:.;?!][ ]+                # or of subsentence...
7868
                      |  [ ][\'"“‘(\[][ ]* )        # or of inserted subphrase...
7869 35
                      ( ' . $small_words_rx . ' ) \\b # ...followed by small word
7870
                     ~uxi',
7871
            /**
7872
             * @param string[] $matches
7873
             *
7874
             * @return string
7875
             */
7876
            static function (array $matches) use ($encoding): string {
7877 11
                return $matches[1] . static::ucfirst($matches[2], $encoding);
7878 35
            },
7879 35
            $str
7880
        );
7881
7882
        // ...and end of title
7883 35
        $str = (string) \preg_replace_callback(
7884 35
            '~\\b ( ' . $small_words_rx . ' ) # small word...
7885
                      (?= [[:punct:]]* \Z          # ...at the end of the title...
7886
                      |   [\'"’”)\]] [ ] )         # ...or of an inserted subphrase?
7887
                     ~uxi',
7888
            /**
7889
             * @param string[] $matches
7890
             *
7891
             * @return string
7892
             */
7893
            static function (array $matches) use ($encoding): string {
7894 3
                return static::ucfirst($matches[1], $encoding);
7895 35
            },
7896 35
            $str
7897
        );
7898
7899
        // Exceptions for small words in hyphenated compound words.
7900
        // e.g. "in-flight" -> In-Flight
7901 35
        $str = (string) \preg_replace_callback(
7902
            '~\\b
7903
                        (?<! -)                   # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (in-flight)
7904 35
                        ( ' . $small_words_rx . ' )
7905
                        (?= -[[:alpha:]]+)        # lookahead for "-someword"
7906
                       ~uxi',
7907
            /**
7908
             * @param string[] $matches
7909
             *
7910
             * @return string
7911
             */
7912
            static function (array $matches) use ($encoding): string {
7913
                return static::ucfirst($matches[1], $encoding);
7914 35
            },
7915 35
            $str
7916
        );
7917
7918
        // e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
7919 35
        $str = (string) \preg_replace_callback(
7920
            '~\\b
7921
                      (?<!…)                    # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (stand-in)
7922
                      ( [[:alpha:]]+- )         # $1 = first word and hyphen, should already be properly capped
7923 35
                      ( ' . $small_words_rx . ' ) # ...followed by small word
7924
                      (?!	- )                 # Negative lookahead for another -
7925
                     ~uxi',
7926
            /**
7927
             * @param string[] $matches
7928
             *
7929
             * @return string
7930
             */
7931
            static function (array $matches) use ($encoding): string {
7932
                return $matches[1] . static::ucfirst($matches[2], $encoding);
7933 35
            },
7934 35
            $str
7935
        );
7936
7937 35
        return $str;
7938
    }
7939
7940
    /**
7941
     * Get a binary representation of a specific string.
7942
     *
7943
     * @param string $str <p>The input string.</p>
7944
     *
7945
     * @return false|string
7946
     *                      <p>false on error</p>
7947
     */
7948
    public static function str_to_binary(string $str)
7949
    {
7950
        /** @var array|false $value - needed for PhpStan (stubs error) */
7951 2
        $value = \unpack('H*', $str);
7952 2
        if ($value === false) {
7953
            return false;
7954
        }
7955
7956
        /** @noinspection OffsetOperationsInspection */
7957 2
        return \base_convert($value[1], 16, 2);
7958
    }
7959
7960
    /**
7961
     * @param string   $str
7962
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
7963
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
7964
     *
7965
     * @return string[]
7966
     */
7967
    public static function str_to_lines(string $str, bool $remove_empty_values = false, int $remove_short_values = null): array
7968
    {
7969 17
        if ($str === '') {
7970 1
            return $remove_empty_values === true ? [] : [''];
7971
        }
7972
7973 16
        if (self::$SUPPORT['mbstring'] === true) {
7974
            /** @noinspection PhpComposerExtensionStubsInspection */
7975 16
            $return = \mb_split("[\r\n]{1,2}", $str);
7976
        } else {
7977
            $return = \preg_split("/[\r\n]{1,2}/u", $str);
7978
        }
7979
7980 16
        if ($return === false) {
7981
            return $remove_empty_values === true ? [] : [''];
7982
        }
7983
7984
        if (
7985 16
            $remove_short_values === null
7986
            &&
7987 16
            $remove_empty_values === false
7988
        ) {
7989 16
            return $return;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $return returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
7990
        }
7991
7992
        return self::reduce_string_array(
7993
            $return,
7994
            $remove_empty_values,
7995
            $remove_short_values
7996
        );
7997
    }
7998
7999
    /**
8000
     * Convert a string into an array of words.
8001
     *
8002
     * @param string   $str
8003
     * @param string   $char_list           <p>Additional chars for the definition of "words".</p>
8004
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
8005
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
8006
     *
8007
     * @return string[]
8008
     */
8009
    public static function str_to_words(
8010
        string $str,
8011
        string $char_list = '',
8012
        bool $remove_empty_values = false,
8013
        int $remove_short_values = null
8014
    ): array {
8015 13
        if ($str === '') {
8016 4
            return $remove_empty_values === true ? [] : [''];
8017
        }
8018
8019 13
        $char_list = self::rxClass($char_list, '\pL');
8020
8021 13
        $return = \preg_split("/({$char_list}+(?:[\p{Pd}’']{$char_list}+)*)/u", $str, -1, \PREG_SPLIT_DELIM_CAPTURE);
8022 13
        if ($return === false) {
8023
            return $remove_empty_values === true ? [] : [''];
8024
        }
8025
8026
        if (
8027 13
            $remove_short_values === null
8028
            &&
8029 13
            $remove_empty_values === false
8030
        ) {
8031 13
            return $return;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $return returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
8032
        }
8033
8034 2
        $tmp_return = self::reduce_string_array(
8035 2
            $return,
8036 2
            $remove_empty_values,
8037 2
            $remove_short_values
8038
        );
8039
8040 2
        foreach ($tmp_return as &$item) {
8041 2
            $item = (string) $item;
8042
        }
8043
8044 2
        return $tmp_return;
8045
    }
8046
8047
    /**
8048
     * alias for "UTF8::to_ascii()"
8049
     *
8050
     * @param string $str
8051
     * @param string $unknown
8052
     * @param bool   $strict
8053
     *
8054
     * @return string
8055
     *
8056
     * @see UTF8::to_ascii()
8057
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
8058
     */
8059
    public static function str_transliterate(
8060
        string $str,
8061
        string $unknown = '?',
8062
        bool $strict = false
8063
    ): string {
8064 7
        return self::to_ascii($str, $unknown, $strict);
8065
    }
8066
8067
    /**
8068
     * Truncates the string to a given length. If $substring is provided, and
8069
     * truncating occurs, the string is further truncated so that the substring
8070
     * may be appended without exceeding the desired length.
8071
     *
8072
     * @param string $str
8073
     * @param int    $length    <p>Desired length of the truncated string.</p>
8074
     * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
8075
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8076
     *
8077
     * @return string string after truncating
8078
     */
8079
    public static function str_truncate(
8080
        string $str,
8081
        int $length,
8082
        string $substring = '',
8083
        string $encoding = 'UTF-8'
8084
    ): string {
8085 22
        if ($str === '') {
8086
            return '';
8087
        }
8088
8089 22
        if ($encoding === 'UTF-8') {
8090 10
            if ($length >= (int) \mb_strlen($str)) {
8091 2
                return $str;
8092
            }
8093
8094 8
            if ($substring !== '') {
8095 4
                $length -= (int) \mb_strlen($substring);
8096
8097
                /** @noinspection UnnecessaryCastingInspection */
8098 4
                return (string) \mb_substr($str, 0, $length) . $substring;
8099
            }
8100
8101
            /** @noinspection UnnecessaryCastingInspection */
8102 4
            return (string) \mb_substr($str, 0, $length);
8103
        }
8104
8105 12
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8106
8107 12
        if ($length >= (int) self::strlen($str, $encoding)) {
8108 2
            return $str;
8109
        }
8110
8111 10
        if ($substring !== '') {
8112 6
            $length -= (int) self::strlen($substring, $encoding);
8113
        }
8114
8115
        return (
8116 10
               (string) self::substr(
8117 10
                   $str,
8118 10
                   0,
8119 10
                   $length,
8120 10
                   $encoding
8121
               )
8122 10
               ) . $substring;
8123
    }
8124
8125
    /**
8126
     * Truncates the string to a given length, while ensuring that it does not
8127
     * split words. If $substring is provided, and truncating occurs, the
8128
     * string is further truncated so that the substring may be appended without
8129
     * exceeding the desired length.
8130
     *
8131
     * @param string $str
8132
     * @param int    $length                                 <p>Desired length of the truncated string.</p>
8133
     * @param string $substring                              [optional] <p>The substring to append if it can fit. Default:
8134
     *                                                       ''</p>
8135
     * @param string $encoding                               [optional] <p>Default: 'UTF-8'</p>
8136
     * @param bool   $ignore_do_not_split_words_for_one_word [optional] <p>Default: false</p>
8137
     *
8138
     * @return string string after truncating
8139
     */
8140
    public static function str_truncate_safe(
8141
        string $str,
8142
        int $length,
8143
        string $substring = '',
8144
        string $encoding = 'UTF-8',
8145
        bool $ignore_do_not_split_words_for_one_word = false
8146
    ): string {
8147 47
        if ($str === '' || $length <= 0) {
8148 1
            return $substring;
8149
        }
8150
8151 47
        if ($encoding === 'UTF-8') {
8152 21
            if ($length >= (int) \mb_strlen($str)) {
8153 5
                return $str;
8154
            }
8155
8156
            // need to further trim the string so we can append the substring
8157 17
            $length -= (int) \mb_strlen($substring);
8158 17
            if ($length <= 0) {
8159 1
                return $substring;
8160
            }
8161
8162
            /** @var false|string $truncated - needed for PhpStan (stubs error) */
8163 17
            $truncated = \mb_substr($str, 0, $length);
8164 17
            if ($truncated === false) {
8165
                return '';
8166
            }
8167
8168
            // if the last word was truncated
8169 17
            $space_position = \mb_strpos($str, ' ', $length - 1);
8170 17
            if ($space_position !== $length) {
8171
                // find pos of the last occurrence of a space, get up to that
8172 13
                $last_position = \mb_strrpos($truncated, ' ', 0);
8173
8174
                if (
8175 13
                    $last_position !== false
8176
                    ||
8177 13
                    ($space_position !== false && $ignore_do_not_split_words_for_one_word === false)
8178
                ) {
8179 17
                    $truncated = (string) \mb_substr($truncated, 0, (int) $last_position);
8180
                }
8181
            }
8182
        } else {
8183 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8184
8185 26
            if ($length >= (int) self::strlen($str, $encoding)) {
8186 4
                return $str;
8187
            }
8188
8189
            // need to further trim the string so we can append the substring
8190 22
            $length -= (int) self::strlen($substring, $encoding);
8191 22
            if ($length <= 0) {
8192
                return $substring;
8193
            }
8194
8195 22
            $truncated = self::substr($str, 0, $length, $encoding);
8196
8197 22
            if ($truncated === false) {
8198
                return '';
8199
            }
8200
8201
            // if the last word was truncated
8202 22
            $space_position = self::strpos($str, ' ', $length - 1, $encoding);
8203 22
            if ($space_position !== $length) {
8204
                // find pos of the last occurrence of a space, get up to that
8205 12
                $last_position = self::strrpos($truncated, ' ', 0, $encoding);
8206
8207
                if (
8208 12
                    $last_position !== false
8209
                    ||
8210 12
                    ($space_position !== false && $ignore_do_not_split_words_for_one_word === false)
8211
                ) {
8212 9
                    $truncated = (string) self::substr($truncated, 0, (int) $last_position, $encoding);
8213
                }
8214
            }
8215
        }
8216
8217 39
        return $truncated . $substring;
8218
    }
8219
8220
    /**
8221
     * Returns a lowercase and trimmed string separated by underscores.
8222
     * Underscores are inserted before uppercase characters (with the exception
8223
     * of the first character of the string), and in place of spaces as well as
8224
     * dashes.
8225
     *
8226
     * @param string $str
8227
     *
8228
     * @return string the underscored string
8229
     */
8230
    public static function str_underscored(string $str): string
8231
    {
8232 16
        return self::str_delimit($str, '_');
8233
    }
8234
8235
    /**
8236
     * Returns an UpperCamelCase version of the supplied string. It trims
8237
     * surrounding spaces, capitalizes letters following digits, spaces, dashes
8238
     * and underscores, and removes spaces, dashes, underscores.
8239
     *
8240
     * @param string      $str                           <p>The input string.</p>
8241
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
8242
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
8243
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
8244
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
8245
     *
8246
     * @return string string in UpperCamelCase
8247
     */
8248
    public static function str_upper_camelize(
8249
        string $str,
8250
        string $encoding = 'UTF-8',
8251
        bool $clean_utf8 = false,
8252
        string $lang = null,
8253
        bool $try_to_keep_the_string_length = false
8254
    ): string {
8255 13
        return self::ucfirst(self::str_camelize($str, $encoding), $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
8256
    }
8257
8258
    /**
8259
     * alias for "UTF8::ucfirst()"
8260
     *
8261
     * @param string      $str
8262
     * @param string      $encoding
8263
     * @param bool        $clean_utf8
8264
     * @param string|null $lang
8265
     * @param bool        $try_to_keep_the_string_length
8266
     *
8267
     * @return string
8268
     *
8269
     * @see UTF8::ucfirst()
8270
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
8271
     */
8272
    public static function str_upper_first(
8273
        string $str,
8274
        string $encoding = 'UTF-8',
8275
        bool $clean_utf8 = false,
8276
        string $lang = null,
8277
        bool $try_to_keep_the_string_length = false
8278
    ): string {
8279 5
        return self::ucfirst(
8280 5
            $str,
8281 5
            $encoding,
8282 5
            $clean_utf8,
8283 5
            $lang,
8284 5
            $try_to_keep_the_string_length
8285
        );
8286
    }
8287
8288
    /**
8289
     * Get the number of words in a specific string.
8290
     *
8291
     * @param string $str       <p>The input string.</p>
8292
     * @param int    $format    [optional] <p>
8293
     *                          <strong>0</strong> => return a number of words (default)<br>
8294
     *                          <strong>1</strong> => return an array of words<br>
8295
     *                          <strong>2</strong> => return an array of words with word-offset as key
8296
     *                          </p>
8297
     * @param string $char_list [optional] <p>Additional chars that contains to words and do not start a new word.</p>
8298
     *
8299
     * @return int|string[] The number of words in the string
8300
     */
8301
    public static function str_word_count(string $str, int $format = 0, string $char_list = '')
8302
    {
8303 2
        $str_parts = self::str_to_words($str, $char_list);
8304
8305 2
        $len = \count($str_parts);
8306
8307 2
        if ($format === 1) {
8308 2
            $number_of_words = [];
8309 2
            for ($i = 1; $i < $len; $i += 2) {
8310 2
                $number_of_words[] = $str_parts[$i];
8311
            }
8312 2
        } elseif ($format === 2) {
8313 2
            $number_of_words = [];
8314 2
            $offset = (int) self::strlen($str_parts[0]);
8315 2
            for ($i = 1; $i < $len; $i += 2) {
8316 2
                $number_of_words[$offset] = $str_parts[$i];
8317 2
                $offset += (int) self::strlen($str_parts[$i]) + (int) self::strlen($str_parts[$i + 1]);
8318
            }
8319
        } else {
8320 2
            $number_of_words = (int) (($len - 1) / 2);
8321
        }
8322
8323 2
        return $number_of_words;
8324
    }
8325
8326
    /**
8327
     * Case-insensitive string comparison.
8328
     *
8329
     * INFO: Case-insensitive version of UTF8::strcmp()
8330
     *
8331
     * @param string $str1     <p>The first string.</p>
8332
     * @param string $str2     <p>The second string.</p>
8333
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8334
     *
8335
     * @return int
8336
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
8337
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
8338
     *             <strong>0</strong> if they are equal
8339
     */
8340
    public static function strcasecmp(
8341
        string $str1,
8342
        string $str2,
8343
        string $encoding = 'UTF-8'
8344
    ): int {
8345 23
        return self::strcmp(
8346 23
            self::strtocasefold(
8347 23
                $str1,
8348 23
                true,
8349 23
                false,
8350 23
                $encoding,
8351 23
                null,
8352 23
                false
8353
            ),
8354 23
            self::strtocasefold(
8355 23
                $str2,
8356 23
                true,
8357 23
                false,
8358 23
                $encoding,
8359 23
                null,
8360 23
                false
8361
            )
8362
        );
8363
    }
8364
8365
    /**
8366
     * alias for "UTF8::strstr()"
8367
     *
8368
     * @param string $haystack
8369
     * @param string $needle
8370
     * @param bool   $before_needle
8371
     * @param string $encoding
8372
     * @param bool   $clean_utf8
8373
     *
8374
     * @return false|string
8375
     *
8376
     * @see UTF8::strstr()
8377
     * @deprecated <p>please use "UTF8::strstr()"</p>
8378
     */
8379
    public static function strchr(
8380
        string $haystack,
8381
        string $needle,
8382
        bool $before_needle = false,
8383
        string $encoding = 'UTF-8',
8384
        bool $clean_utf8 = false
8385
    ) {
8386 2
        return self::strstr(
8387 2
            $haystack,
8388 2
            $needle,
8389 2
            $before_needle,
8390 2
            $encoding,
8391 2
            $clean_utf8
8392
        );
8393
    }
8394
8395
    /**
8396
     * Case-sensitive string comparison.
8397
     *
8398
     * @param string $str1 <p>The first string.</p>
8399
     * @param string $str2 <p>The second string.</p>
8400
     *
8401
     * @return int
8402
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
8403
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
8404
     *             <strong>0</strong> if they are equal
8405
     */
8406
    public static function strcmp(string $str1, string $str2): int
8407
    {
8408 29
        if ($str1 === $str2) {
8409 21
            return 0;
8410
        }
8411
8412 24
        return \strcmp(
8413 24
            \Normalizer::normalize($str1, \Normalizer::NFD),
8414 24
            \Normalizer::normalize($str2, \Normalizer::NFD)
8415
        );
8416
    }
8417
8418
    /**
8419
     * Find length of initial segment not matching mask.
8420
     *
8421
     * @param string $str
8422
     * @param string $char_list
8423
     * @param int    $offset
8424
     * @param int    $length
8425
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
8426
     *
8427
     * @return int
8428
     */
8429
    public static function strcspn(
8430
        string $str,
8431
        string $char_list,
8432
        int $offset = null,
8433
        int $length = null,
8434
        string $encoding = 'UTF-8'
8435
    ): int {
8436 12
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
8437
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8438
        }
8439
8440 12
        if ($char_list === '') {
8441 2
            return (int) self::strlen($str, $encoding);
8442
        }
8443
8444 11
        if ($offset !== null || $length !== null) {
8445 3
            if ($encoding === 'UTF-8') {
8446 3
                if ($length === null) {
8447
                    /** @noinspection UnnecessaryCastingInspection */
8448 2
                    $str_tmp = \mb_substr($str, (int) $offset);
8449
                } else {
8450
                    /** @noinspection UnnecessaryCastingInspection */
8451 3
                    $str_tmp = \mb_substr($str, (int) $offset, $length);
8452
                }
8453
            } else {
8454
                /** @noinspection UnnecessaryCastingInspection */
8455
                $str_tmp = self::substr($str, (int) $offset, $length, $encoding);
8456
            }
8457
8458 3
            if ($str_tmp === false) {
8459
                return 0;
8460
            }
8461
8462
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
8463 3
            $str = $str_tmp;
8464
        }
8465
8466 11
        if ($str === '') {
8467 2
            return 0;
8468
        }
8469
8470 10
        $matches = [];
8471 10
        if (\preg_match('/^(.*?)' . self::rxClass($char_list) . '/us', $str, $matches)) {
8472 9
            $return = self::strlen($matches[1], $encoding);
8473 9
            if ($return === false) {
8474
                return 0;
8475
            }
8476
8477 9
            return $return;
8478
        }
8479
8480 2
        return (int) self::strlen($str, $encoding);
8481
    }
8482
8483
    /**
8484
     * alias for "UTF8::stristr()"
8485
     *
8486
     * @param string $haystack
8487
     * @param string $needle
8488
     * @param bool   $before_needle
8489
     * @param string $encoding
8490
     * @param bool   $clean_utf8
8491
     *
8492
     * @return false|string
8493
     *
8494
     * @see UTF8::stristr()
8495
     * @deprecated <p>please use "UTF8::stristr()"</p>
8496
     */
8497
    public static function strichr(
8498
        string $haystack,
8499
        string $needle,
8500
        bool $before_needle = false,
8501
        string $encoding = 'UTF-8',
8502
        bool $clean_utf8 = false
8503
    ) {
8504 1
        return self::stristr(
8505 1
            $haystack,
8506 1
            $needle,
8507 1
            $before_needle,
8508 1
            $encoding,
8509 1
            $clean_utf8
8510
        );
8511
    }
8512
8513
    /**
8514
     * Create a UTF-8 string from code points.
8515
     *
8516
     * INFO: opposite to UTF8::codepoints()
8517
     *
8518
     * @param array $array <p>Integer or Hexadecimal codepoints.</p>
8519
     *
8520
     * @return string UTF-8 encoded string
8521
     */
8522
    public static function string(array $array): string
8523
    {
8524 4
        return \implode(
8525 4
            '',
8526 4
            \array_map(
8527
                [
8528 4
                    self::class,
8529
                    'chr',
8530
                ],
8531 4
                $array
8532
            )
8533
        );
8534
    }
8535
8536
    /**
8537
     * Checks if string starts with "BOM" (Byte Order Mark Character) character.
8538
     *
8539
     * @param string $str <p>The input string.</p>
8540
     *
8541
     * @return bool
8542
     *              <strong>true</strong> if the string has BOM at the start,<br>
8543
     *              <strong>false</strong> otherwise
8544
     */
8545
    public static function string_has_bom(string $str): bool
8546
    {
8547
        /** @noinspection PhpUnusedLocalVariableInspection */
8548 6
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
8549 6
            if (\strpos($str, $bom_string) === 0) {
8550 6
                return true;
8551
            }
8552
        }
8553
8554 6
        return false;
8555
    }
8556
8557
    /**
8558
     * Strip HTML and PHP tags from a string + clean invalid UTF-8.
8559
     *
8560
     * @see http://php.net/manual/en/function.strip-tags.php
8561
     *
8562
     * @param string $str            <p>
8563
     *                               The input string.
8564
     *                               </p>
8565
     * @param string $allowable_tags [optional] <p>
8566
     *                               You can use the optional second parameter to specify tags which should
8567
     *                               not be stripped.
8568
     *                               </p>
8569
     *                               <p>
8570
     *                               HTML comments and PHP tags are also stripped. This is hardcoded and
8571
     *                               can not be changed with allowable_tags.
8572
     *                               </p>
8573
     * @param bool   $clean_utf8     [optional] <p>Remove non UTF-8 chars from the string.</p>
8574
     *
8575
     * @return string
8576
     *                <p>The stripped string.</p>
8577
     */
8578
    public static function strip_tags(
8579
        string $str,
8580
        string $allowable_tags = null,
8581
        bool $clean_utf8 = false
8582
    ): string {
8583 4
        if ($str === '') {
8584 1
            return '';
8585
        }
8586
8587 4
        if ($clean_utf8 === true) {
8588 2
            $str = self::clean($str);
8589
        }
8590
8591 4
        if ($allowable_tags === null) {
8592 4
            return \strip_tags($str);
8593
        }
8594
8595 2
        return \strip_tags($str, $allowable_tags);
8596
    }
8597
8598
    /**
8599
     * Strip all whitespace characters. This includes tabs and newline
8600
     * characters, as well as multibyte whitespace such as the thin space
8601
     * and ideographic space.
8602
     *
8603
     * @param string $str
8604
     *
8605
     * @return string
8606
     */
8607
    public static function strip_whitespace(string $str): string
8608
    {
8609 36
        if ($str === '') {
8610 3
            return '';
8611
        }
8612
8613 33
        return (string) \preg_replace('/[[:space:]]+/u', '', $str);
8614
    }
8615
8616
    /**
8617
     * Find the position of the first occurrence of a substring in a string, case-insensitive.
8618
     *
8619
     * @see http://php.net/manual/en/function.mb-stripos.php
8620
     *
8621
     * @param string $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
8622
     * @param string $needle     <p>The string to find in haystack.</p>
8623
     * @param int    $offset     [optional] <p>The position in haystack to start searching.</p>
8624
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
8625
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
8626
     *
8627
     * @return false|int
8628
     *                   Return the <strong>(int)</strong> numeric position of the first occurrence of needle in the
8629
     *                   haystack string,<br> or <strong>false</strong> if needle is not found
8630
     */
8631
    public static function stripos(
8632
        string $haystack,
8633
        string $needle,
8634
        int $offset = 0,
8635
        $encoding = 'UTF-8',
8636
        bool $clean_utf8 = false
8637
    ) {
8638 24
        if ($haystack === '' || $needle === '') {
8639 5
            return false;
8640
        }
8641
8642 23
        if ($clean_utf8 === true) {
8643
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
8644
            // if invalid characters are found in $haystack before $needle
8645 1
            $haystack = self::clean($haystack);
8646 1
            $needle = self::clean($needle);
8647
        }
8648
8649 23
        if (self::$SUPPORT['mbstring'] === true) {
8650 23
            if ($encoding === 'UTF-8') {
8651 23
                return \mb_stripos($haystack, $needle, $offset);
8652
            }
8653
8654 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8655
8656 3
            return \mb_stripos($haystack, $needle, $offset, $encoding);
8657
        }
8658
8659 2
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8660
8661
        if (
8662 2
            $encoding === 'UTF-8' // INFO: "grapheme_stripos()" can't handle other encodings
8663
            &&
8664 2
            $offset >= 0 // grapheme_stripos() can't handle negative offset
8665
            &&
8666 2
            self::$SUPPORT['intl'] === true
8667
        ) {
8668
            $return_tmp = \grapheme_stripos($haystack, $needle, $offset);
8669
            if ($return_tmp !== false) {
8670
                return $return_tmp;
8671
            }
8672
        }
8673
8674
        //
8675
        // fallback for ascii only
8676
        //
8677
8678 2
        if (ASCII::is_ascii($haystack . $needle)) {
8679
            return \stripos($haystack, $needle, $offset);
8680
        }
8681
8682
        //
8683
        // fallback via vanilla php
8684
        //
8685
8686 2
        $haystack = self::strtocasefold($haystack, true, false, $encoding, null, false);
8687 2
        $needle = self::strtocasefold($needle, true, false, $encoding, null, false);
8688
8689 2
        return self::strpos($haystack, $needle, $offset, $encoding);
8690
    }
8691
8692
    /**
8693
     * Returns all of haystack starting from and including the first occurrence of needle to the end.
8694
     *
8695
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
8696
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
8697
     * @param bool   $before_needle [optional] <p>
8698
     *                              If <b>TRUE</b>, it returns the part of the
8699
     *                              haystack before the first occurrence of the needle (excluding the needle).
8700
     *                              </p>
8701
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
8702
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
8703
     *
8704
     * @return false|string
8705
     *                      <p>A sub-string,<br>or <strong>false</strong> if needle is not found.</p>
8706
     */
8707
    public static function stristr(
8708
        string $haystack,
8709
        string $needle,
8710
        bool $before_needle = false,
8711
        string $encoding = 'UTF-8',
8712
        bool $clean_utf8 = false
8713
    ) {
8714 12
        if ($haystack === '' || $needle === '') {
8715 3
            return false;
8716
        }
8717
8718 9
        if ($clean_utf8 === true) {
8719
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
8720
            // if invalid characters are found in $haystack before $needle
8721 1
            $needle = self::clean($needle);
8722 1
            $haystack = self::clean($haystack);
8723
        }
8724
8725 9
        if (!$needle) {
8726
            return $haystack;
8727
        }
8728
8729 9
        if (self::$SUPPORT['mbstring'] === true) {
8730 9
            if ($encoding === 'UTF-8') {
8731 9
                return \mb_stristr($haystack, $needle, $before_needle);
8732
            }
8733
8734 1
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8735
8736 1
            return \mb_stristr($haystack, $needle, $before_needle, $encoding);
8737
        }
8738
8739
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8740
8741
        if (
8742
            $encoding !== 'UTF-8'
8743
            &&
8744
            self::$SUPPORT['mbstring'] === false
8745
        ) {
8746
            \trigger_error('UTF8::stristr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
8747
        }
8748
8749
        if (
8750
            $encoding === 'UTF-8' // INFO: "grapheme_stristr()" can't handle other encodings
8751
            &&
8752
            self::$SUPPORT['intl'] === true
8753
        ) {
8754
            $return_tmp = \grapheme_stristr($haystack, $needle, $before_needle);
8755
            if ($return_tmp !== false) {
8756
                return $return_tmp;
8757
            }
8758
        }
8759
8760
        if (ASCII::is_ascii($needle . $haystack)) {
8761
            return \stristr($haystack, $needle, $before_needle);
8762
        }
8763
8764
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/usi', $haystack, $match);
8765
8766
        if (!isset($match[1])) {
8767
            return false;
8768
        }
8769
8770
        if ($before_needle) {
8771
            return $match[1];
8772
        }
8773
8774
        return self::substr($haystack, (int) self::strlen($match[1], $encoding), null, $encoding);
8775
    }
8776
8777
    /**
8778
     * Get the string length, not the byte-length!
8779
     *
8780
     * @see http://php.net/manual/en/function.mb-strlen.php
8781
     *
8782
     * @param string $str        <p>The string being checked for length.</p>
8783
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
8784
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
8785
     *
8786
     * @return false|int
8787
     *                   <p>
8788
     *                   The number <strong>(int)</strong> of characters in the string $str having character encoding
8789
     *                   $encoding.
8790
     *                   (One multi-byte character counted as +1).
8791
     *                   <br>
8792
     *                   Can return <strong>false</strong>, if e.g. mbstring is not installed and we process invalid
8793
     *                   chars.
8794
     *                   </p>
8795
     */
8796
    public static function strlen(
8797
        string $str,
8798
        string $encoding = 'UTF-8',
8799
        bool $clean_utf8 = false
8800
    ) {
8801 173
        if ($str === '') {
8802 21
            return 0;
8803
        }
8804
8805 171
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
8806 12
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8807
        }
8808
8809 171
        if ($clean_utf8 === true) {
8810
            // "mb_strlen" and "\iconv_strlen" returns wrong length,
8811
            // if invalid characters are found in $str
8812 4
            $str = self::clean($str);
8813
        }
8814
8815
        //
8816
        // fallback via mbstring
8817
        //
8818
8819 171
        if (self::$SUPPORT['mbstring'] === true) {
8820 165
            if ($encoding === 'UTF-8') {
8821 165
                return \mb_strlen($str);
8822
            }
8823
8824 4
            return \mb_strlen($str, $encoding);
8825
        }
8826
8827
        //
8828
        // fallback for binary || ascii only
8829
        //
8830
8831
        if (
8832 8
            $encoding === 'CP850'
8833
            ||
8834 8
            $encoding === 'ASCII'
8835
        ) {
8836
            return \strlen($str);
8837
        }
8838
8839
        if (
8840 8
            $encoding !== 'UTF-8'
8841
            &&
8842 8
            self::$SUPPORT['mbstring'] === false
8843
            &&
8844 8
            self::$SUPPORT['iconv'] === false
8845
        ) {
8846 2
            \trigger_error('UTF8::strlen() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
8847
        }
8848
8849
        //
8850
        // fallback via iconv
8851
        //
8852
8853 8
        if (self::$SUPPORT['iconv'] === true) {
8854
            $return_tmp = \iconv_strlen($str, $encoding);
8855
            if ($return_tmp !== false) {
8856
                return $return_tmp;
8857
            }
8858
        }
8859
8860
        //
8861
        // fallback via intl
8862
        //
8863
8864
        if (
8865 8
            $encoding === 'UTF-8' // INFO: "grapheme_strlen()" can't handle other encodings
8866
            &&
8867 8
            self::$SUPPORT['intl'] === true
8868
        ) {
8869
            $return_tmp = \grapheme_strlen($str);
8870
            if ($return_tmp !== null) {
8871
                return $return_tmp;
8872
            }
8873
        }
8874
8875
        //
8876
        // fallback for ascii only
8877
        //
8878
8879 8
        if (ASCII::is_ascii($str)) {
8880 4
            return \strlen($str);
8881
        }
8882
8883
        //
8884
        // fallback via vanilla php
8885
        //
8886
8887 8
        \preg_match_all('/./us', $str, $parts);
8888
8889 8
        $return_tmp = \count($parts[0]);
8890 8
        if ($return_tmp === 0) {
8891
            return false;
8892
        }
8893
8894 8
        return $return_tmp;
8895
    }
8896
8897
    /**
8898
     * Get string length in byte.
8899
     *
8900
     * @param string $str
8901
     *
8902
     * @return int
8903
     */
8904
    public static function strlen_in_byte(string $str): int
8905
    {
8906
        if ($str === '') {
8907
            return 0;
8908
        }
8909
8910
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
8911
            // "mb_" is available if overload is used, so use it ...
8912
            return \mb_strlen($str, 'CP850'); // 8-BIT
8913
        }
8914
8915
        return \strlen($str);
8916
    }
8917
8918
    /**
8919
     * Case-insensitive string comparisons using a "natural order" algorithm.
8920
     *
8921
     * INFO: natural order version of UTF8::strcasecmp()
8922
     *
8923
     * @param string $str1     <p>The first string.</p>
8924
     * @param string $str2     <p>The second string.</p>
8925
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8926
     *
8927
     * @return int
8928
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
8929
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
8930
     *             <strong>0</strong> if they are equal
8931
     */
8932
    public static function strnatcasecmp(string $str1, string $str2, string $encoding = 'UTF-8'): int
8933
    {
8934 2
        return self::strnatcmp(
8935 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
8936 2
            self::strtocasefold($str2, true, false, $encoding, null, false)
8937
        );
8938
    }
8939
8940
    /**
8941
     * String comparisons using a "natural order" algorithm
8942
     *
8943
     * INFO: natural order version of UTF8::strcmp()
8944
     *
8945
     * @see http://php.net/manual/en/function.strnatcmp.php
8946
     *
8947
     * @param string $str1 <p>The first string.</p>
8948
     * @param string $str2 <p>The second string.</p>
8949
     *
8950
     * @return int
8951
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
8952
     *             <strong>&gt; 0</strong> if str1 is greater than str2;<br>
8953
     *             <strong>0</strong> if they are equal
8954
     */
8955
    public static function strnatcmp(string $str1, string $str2): int
8956
    {
8957 4
        if ($str1 === $str2) {
8958 4
            return 0;
8959
        }
8960
8961 4
        return \strnatcmp(
8962 4
            (string) self::strtonatfold($str1),
8963 4
            (string) self::strtonatfold($str2)
8964
        );
8965
    }
8966
8967
    /**
8968
     * Case-insensitive string comparison of the first n characters.
8969
     *
8970
     * @see http://php.net/manual/en/function.strncasecmp.php
8971
     *
8972
     * @param string $str1     <p>The first string.</p>
8973
     * @param string $str2     <p>The second string.</p>
8974
     * @param int    $len      <p>The length of strings to be used in the comparison.</p>
8975
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8976
     *
8977
     * @return int
8978
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
8979
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
8980
     *             <strong>0</strong> if they are equal
8981
     */
8982
    public static function strncasecmp(
8983
        string $str1,
8984
        string $str2,
8985
        int $len,
8986
        string $encoding = 'UTF-8'
8987
    ): int {
8988 2
        return self::strncmp(
8989 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
8990 2
            self::strtocasefold($str2, true, false, $encoding, null, false),
8991 2
            $len
8992
        );
8993
    }
8994
8995
    /**
8996
     * String comparison of the first n characters.
8997
     *
8998
     * @see http://php.net/manual/en/function.strncmp.php
8999
     *
9000
     * @param string $str1     <p>The first string.</p>
9001
     * @param string $str2     <p>The second string.</p>
9002
     * @param int    $len      <p>Number of characters to use in the comparison.</p>
9003
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9004
     *
9005
     * @return int
9006
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
9007
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
9008
     *             <strong>0</strong> if they are equal
9009
     */
9010
    public static function strncmp(
9011
        string $str1,
9012
        string $str2,
9013
        int $len,
9014
        string $encoding = 'UTF-8'
9015
    ): int {
9016 4
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9017
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9018
        }
9019
9020 4
        if ($encoding === 'UTF-8') {
9021 4
            $str1 = (string) \mb_substr($str1, 0, $len);
9022 4
            $str2 = (string) \mb_substr($str2, 0, $len);
9023
        } else {
9024
            $str1 = (string) self::substr($str1, 0, $len, $encoding);
9025
            $str2 = (string) self::substr($str2, 0, $len, $encoding);
9026
        }
9027
9028 4
        return self::strcmp($str1, $str2);
9029
    }
9030
9031
    /**
9032
     * Search a string for any of a set of characters.
9033
     *
9034
     * @see http://php.net/manual/en/function.strpbrk.php
9035
     *
9036
     * @param string $haystack  <p>The string where char_list is looked for.</p>
9037
     * @param string $char_list <p>This parameter is case-sensitive.</p>
9038
     *
9039
     * @return false|string string starting from the character found, or false if it is not found
9040
     */
9041
    public static function strpbrk(string $haystack, string $char_list)
9042
    {
9043 2
        if ($haystack === '' || $char_list === '') {
9044 2
            return false;
9045
        }
9046
9047 2
        if (\preg_match('/' . self::rxClass($char_list) . '/us', $haystack, $m)) {
9048 2
            return \substr($haystack, (int) \strpos($haystack, $m[0]));
9049
        }
9050
9051 2
        return false;
9052
    }
9053
9054
    /**
9055
     * Find the position of the first occurrence of a substring in a string.
9056
     *
9057
     * @see http://php.net/manual/en/function.mb-strpos.php
9058
     *
9059
     * @param string     $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
9060
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
9061
     * @param int        $offset     [optional] <p>The search offset. If it is not specified, 0 is used.</p>
9062
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9063
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9064
     *
9065
     * @return false|int
9066
     *                   The <strong>(int)</strong> numeric position of the first occurrence of needle in the haystack
9067
     *                   string.<br> If needle is not found it returns false.
9068
     */
9069
    public static function strpos(
9070
        string $haystack,
9071
        $needle,
9072
        int $offset = 0,
9073
        $encoding = 'UTF-8',
9074
        bool $clean_utf8 = false
9075
    ) {
9076 53
        if ($haystack === '') {
9077 4
            return false;
9078
        }
9079
9080
        // iconv and mbstring do not support integer $needle
9081 52
        if ((int) $needle === $needle) {
9082
            $needle = (string) self::chr($needle);
9083
        }
9084 52
        $needle = (string) $needle;
9085
9086 52
        if ($needle === '') {
9087 2
            return false;
9088
        }
9089
9090 52
        if ($clean_utf8 === true) {
9091
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9092
            // if invalid characters are found in $haystack before $needle
9093 3
            $needle = self::clean($needle);
9094 3
            $haystack = self::clean($haystack);
9095
        }
9096
9097 52
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9098 11
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9099
        }
9100
9101
        //
9102
        // fallback via mbstring
9103
        //
9104
9105 52
        if (self::$SUPPORT['mbstring'] === true) {
9106 50
            if ($encoding === 'UTF-8') {
9107 50
                return \mb_strpos($haystack, $needle, $offset);
9108
            }
9109
9110 2
            return \mb_strpos($haystack, $needle, $offset, $encoding);
9111
        }
9112
9113
        //
9114
        // fallback for binary || ascii only
9115
        //
9116
        if (
9117 4
            $encoding === 'CP850'
9118
            ||
9119 4
            $encoding === 'ASCII'
9120
        ) {
9121 2
            return \strpos($haystack, $needle, $offset);
9122
        }
9123
9124
        if (
9125 4
            $encoding !== 'UTF-8'
9126
            &&
9127 4
            self::$SUPPORT['iconv'] === false
9128
            &&
9129 4
            self::$SUPPORT['mbstring'] === false
9130
        ) {
9131 2
            \trigger_error('UTF8::strpos() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9132
        }
9133
9134
        //
9135
        // fallback via intl
9136
        //
9137
9138
        if (
9139 4
            $encoding === 'UTF-8' // INFO: "grapheme_strpos()" can't handle other encodings
9140
            &&
9141 4
            $offset >= 0 // grapheme_strpos() can't handle negative offset
9142
            &&
9143 4
            self::$SUPPORT['intl'] === true
9144
        ) {
9145
            $return_tmp = \grapheme_strpos($haystack, $needle, $offset);
9146
            if ($return_tmp !== false) {
9147
                return $return_tmp;
9148
            }
9149
        }
9150
9151
        //
9152
        // fallback via iconv
9153
        //
9154
9155
        if (
9156 4
            $offset >= 0 // iconv_strpos() can't handle negative offset
9157
            &&
9158 4
            self::$SUPPORT['iconv'] === true
9159
        ) {
9160
            // ignore invalid negative offset to keep compatibility
9161
            // with php < 5.5.35, < 5.6.21, < 7.0.6
9162
            $return_tmp = \iconv_strpos($haystack, $needle, $offset > 0 ? $offset : 0, $encoding);
9163
            if ($return_tmp !== false) {
9164
                return $return_tmp;
9165
            }
9166
        }
9167
9168
        //
9169
        // fallback for ascii only
9170
        //
9171
9172 4
        if (ASCII::is_ascii($haystack . $needle)) {
9173 2
            return \strpos($haystack, $needle, $offset);
9174
        }
9175
9176
        //
9177
        // fallback via vanilla php
9178
        //
9179
9180 4
        $haystack_tmp = self::substr($haystack, $offset, null, $encoding);
9181 4
        if ($haystack_tmp === false) {
9182
            $haystack_tmp = '';
9183
        }
9184 4
        $haystack = (string) $haystack_tmp;
9185
9186 4
        if ($offset < 0) {
9187
            $offset = 0;
9188
        }
9189
9190 4
        $pos = \strpos($haystack, $needle);
9191 4
        if ($pos === false) {
9192 2
            return false;
9193
        }
9194
9195 4
        if ($pos) {
9196 4
            return $offset + (int) self::strlen(\substr($haystack, 0, $pos), $encoding);
9197
        }
9198
9199 2
        return $offset + 0;
9200
    }
9201
9202
    /**
9203
     * Find the position of the first occurrence of a substring in a string.
9204
     *
9205
     * @param string $haystack <p>
9206
     *                         The string being checked.
9207
     *                         </p>
9208
     * @param string $needle   <p>
9209
     *                         The position counted from the beginning of haystack.
9210
     *                         </p>
9211
     * @param int    $offset   [optional] <p>
9212
     *                         The search offset. If it is not specified, 0 is used.
9213
     *                         </p>
9214
     *
9215
     * @return false|int The numeric position of the first occurrence of needle in the
9216
     *                   haystack string. If needle is not found, it returns false.
9217
     */
9218
    public static function strpos_in_byte(string $haystack, string $needle, int $offset = 0)
9219
    {
9220
        if ($haystack === '' || $needle === '') {
9221
            return false;
9222
        }
9223
9224
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9225
            // "mb_" is available if overload is used, so use it ...
9226
            return \mb_strpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
9227
        }
9228
9229
        return \strpos($haystack, $needle, $offset);
9230
    }
9231
9232
    /**
9233
     * Find the last occurrence of a character in a string within another.
9234
     *
9235
     * @see http://php.net/manual/en/function.mb-strrchr.php
9236
     *
9237
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
9238
     * @param string $needle        <p>The string to find in haystack</p>
9239
     * @param bool   $before_needle [optional] <p>
9240
     *                              Determines which portion of haystack
9241
     *                              this function returns.
9242
     *                              If set to true, it returns all of haystack
9243
     *                              from the beginning to the last occurrence of needle.
9244
     *                              If set to false, it returns all of haystack
9245
     *                              from the last occurrence of needle to the end,
9246
     *                              </p>
9247
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9248
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9249
     *
9250
     * @return false|string the portion of haystack or false if needle is not found
9251
     */
9252
    public static function strrchr(
9253
        string $haystack,
9254
        string $needle,
9255
        bool $before_needle = false,
9256
        string $encoding = 'UTF-8',
9257
        bool $clean_utf8 = false
9258
    ) {
9259 2
        if ($haystack === '' || $needle === '') {
9260 2
            return false;
9261
        }
9262
9263 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9264 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9265
        }
9266
9267 2
        if ($clean_utf8 === true) {
9268
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9269
            // if invalid characters are found in $haystack before $needle
9270 2
            $needle = self::clean($needle);
9271 2
            $haystack = self::clean($haystack);
9272
        }
9273
9274
        //
9275
        // fallback via mbstring
9276
        //
9277
9278 2
        if (self::$SUPPORT['mbstring'] === true) {
9279 2
            if ($encoding === 'UTF-8') {
9280 2
                return \mb_strrchr($haystack, $needle, $before_needle);
9281
            }
9282
9283 2
            return \mb_strrchr($haystack, $needle, $before_needle, $encoding);
9284
        }
9285
9286
        //
9287
        // fallback for binary || ascii only
9288
        //
9289
9290
        if (
9291
            $before_needle === false
9292
            &&
9293
            (
9294
                $encoding === 'CP850'
9295
                ||
9296
                $encoding === 'ASCII'
9297
            )
9298
        ) {
9299
            return \strrchr($haystack, $needle);
9300
        }
9301
9302
        if (
9303
            $encoding !== 'UTF-8'
9304
            &&
9305
            self::$SUPPORT['mbstring'] === false
9306
        ) {
9307
            \trigger_error('UTF8::strrchr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9308
        }
9309
9310
        //
9311
        // fallback via iconv
9312
        //
9313
9314
        if (self::$SUPPORT['iconv'] === true) {
9315
            $needle_tmp = self::substr($needle, 0, 1, $encoding);
9316
            if ($needle_tmp === false) {
9317
                return false;
9318
            }
9319
            $needle = (string) $needle_tmp;
9320
9321
            $pos = \iconv_strrpos($haystack, $needle, $encoding);
9322
            if ($pos === false) {
9323
                return false;
9324
            }
9325
9326
            if ($before_needle) {
9327
                return self::substr($haystack, 0, $pos, $encoding);
9328
            }
9329
9330
            return self::substr($haystack, $pos, null, $encoding);
9331
        }
9332
9333
        //
9334
        // fallback via vanilla php
9335
        //
9336
9337
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
9338
        if ($needle_tmp === false) {
9339
            return false;
9340
        }
9341
        $needle = (string) $needle_tmp;
9342
9343
        $pos = self::strrpos($haystack, $needle, 0, $encoding);
9344
        if ($pos === false) {
9345
            return false;
9346
        }
9347
9348
        if ($before_needle) {
9349
            return self::substr($haystack, 0, $pos, $encoding);
9350
        }
9351
9352
        return self::substr($haystack, $pos, null, $encoding);
9353
    }
9354
9355
    /**
9356
     * Reverses characters order in the string.
9357
     *
9358
     * @param string $str      <p>The input string.</p>
9359
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9360
     *
9361
     * @return string the string with characters in the reverse sequence
9362
     */
9363
    public static function strrev(string $str, string $encoding = 'UTF-8'): string
9364
    {
9365 10
        if ($str === '') {
9366 4
            return '';
9367
        }
9368
9369
        // init
9370 8
        $reversed = '';
9371
9372 8
        $str = self::emoji_encode($str, true);
9373
9374 8
        if ($encoding === 'UTF-8') {
9375 8
            if (self::$SUPPORT['intl'] === true) {
9376
                // try "grapheme" first: https://stackoverflow.com/questions/17496493/strrev-dosent-support-utf-8
9377 8
                $i = (int) \grapheme_strlen($str);
9378 8
                while ($i--) {
9379 8
                    $reversed_tmp = \grapheme_substr($str, $i, 1);
9380 8
                    if ($reversed_tmp !== false) {
9381 8
                        $reversed .= $reversed_tmp;
9382
                    }
9383
                }
9384
            } else {
9385
                $i = (int) \mb_strlen($str);
9386 8
                while ($i--) {
9387
                    $reversed_tmp = \mb_substr($str, $i, 1);
9388
                    if ($reversed_tmp !== false) {
9389
                        $reversed .= $reversed_tmp;
9390
                    }
9391
                }
9392
            }
9393
        } else {
9394
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9395
9396
            $i = (int) self::strlen($str, $encoding);
9397
            while ($i--) {
9398
                $reversed_tmp = self::substr($str, $i, 1, $encoding);
9399
                if ($reversed_tmp !== false) {
9400
                    $reversed .= $reversed_tmp;
9401
                }
9402
            }
9403
        }
9404
9405 8
        return self::emoji_decode($reversed, true);
9406
    }
9407
9408
    /**
9409
     * Find the last occurrence of a character in a string within another, case-insensitive.
9410
     *
9411
     * @see http://php.net/manual/en/function.mb-strrichr.php
9412
     *
9413
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
9414
     * @param string $needle        <p>The string to find in haystack.</p>
9415
     * @param bool   $before_needle [optional] <p>
9416
     *                              Determines which portion of haystack
9417
     *                              this function returns.
9418
     *                              If set to true, it returns all of haystack
9419
     *                              from the beginning to the last occurrence of needle.
9420
     *                              If set to false, it returns all of haystack
9421
     *                              from the last occurrence of needle to the end,
9422
     *                              </p>
9423
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9424
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9425
     *
9426
     * @return false|string the portion of haystack or<br>false if needle is not found
9427
     */
9428
    public static function strrichr(
9429
        string $haystack,
9430
        string $needle,
9431
        bool $before_needle = false,
9432
        string $encoding = 'UTF-8',
9433
        bool $clean_utf8 = false
9434
    ) {
9435 3
        if ($haystack === '' || $needle === '') {
9436 2
            return false;
9437
        }
9438
9439 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9440 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9441
        }
9442
9443 3
        if ($clean_utf8 === true) {
9444
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9445
            // if invalid characters are found in $haystack before $needle
9446 2
            $needle = self::clean($needle);
9447 2
            $haystack = self::clean($haystack);
9448
        }
9449
9450
        //
9451
        // fallback via mbstring
9452
        //
9453
9454 3
        if (self::$SUPPORT['mbstring'] === true) {
9455 3
            if ($encoding === 'UTF-8') {
9456 3
                return \mb_strrichr($haystack, $needle, $before_needle);
9457
            }
9458
9459 2
            return \mb_strrichr($haystack, $needle, $before_needle, $encoding);
9460
        }
9461
9462
        //
9463
        // fallback via vanilla php
9464
        //
9465
9466
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
9467
        if ($needle_tmp === false) {
9468
            return false;
9469
        }
9470
        $needle = (string) $needle_tmp;
9471
9472
        $pos = self::strripos($haystack, $needle, 0, $encoding);
9473
        if ($pos === false) {
9474
            return false;
9475
        }
9476
9477
        if ($before_needle) {
9478
            return self::substr($haystack, 0, $pos, $encoding);
9479
        }
9480
9481
        return self::substr($haystack, $pos, null, $encoding);
9482
    }
9483
9484
    /**
9485
     * Find the position of the last occurrence of a substring in a string, case-insensitive.
9486
     *
9487
     * @param string     $haystack   <p>The string to look in.</p>
9488
     * @param int|string $needle     <p>The string to look for.</p>
9489
     * @param int        $offset     [optional] <p>Number of characters to ignore in the beginning or end.</p>
9490
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9491
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9492
     *
9493
     * @return false|int
9494
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
9495
     *                   string.<br>If needle is not found, it returns false.</p>
9496
     */
9497
    public static function strripos(
9498
        string $haystack,
9499
        $needle,
9500
        int $offset = 0,
9501
        string $encoding = 'UTF-8',
9502
        bool $clean_utf8 = false
9503
    ) {
9504 3
        if ($haystack === '') {
9505
            return false;
9506
        }
9507
9508
        // iconv and mbstring do not support integer $needle
9509 3
        if ((int) $needle === $needle && $needle >= 0) {
9510
            $needle = (string) self::chr($needle);
9511
        }
9512 3
        $needle = (string) $needle;
9513
9514 3
        if ($needle === '') {
9515
            return false;
9516
        }
9517
9518 3
        if ($clean_utf8 === true) {
9519
            // mb_strripos() && iconv_strripos() is not tolerant to invalid characters
9520 2
            $needle = self::clean($needle);
9521 2
            $haystack = self::clean($haystack);
9522
        }
9523
9524 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9525 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9526
        }
9527
9528
        //
9529
        // fallback via mbstrig
9530
        //
9531
9532 3
        if (self::$SUPPORT['mbstring'] === true) {
9533 3
            if ($encoding === 'UTF-8') {
9534 3
                return \mb_strripos($haystack, $needle, $offset);
9535
            }
9536
9537
            return \mb_strripos($haystack, $needle, $offset, $encoding);
9538
        }
9539
9540
        //
9541
        // fallback for binary || ascii only
9542
        //
9543
9544
        if (
9545
            $encoding === 'CP850'
9546
            ||
9547
            $encoding === 'ASCII'
9548
        ) {
9549
            return \strripos($haystack, $needle, $offset);
9550
        }
9551
9552
        if (
9553
            $encoding !== 'UTF-8'
9554
            &&
9555
            self::$SUPPORT['mbstring'] === false
9556
        ) {
9557
            \trigger_error('UTF8::strripos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9558
        }
9559
9560
        //
9561
        // fallback via intl
9562
        //
9563
9564
        if (
9565
            $encoding === 'UTF-8' // INFO: "grapheme_strripos()" can't handle other encodings
9566
            &&
9567
            $offset >= 0 // grapheme_strripos() can't handle negative offset
9568
            &&
9569
            self::$SUPPORT['intl'] === true
9570
        ) {
9571
            $return_tmp = \grapheme_strripos($haystack, $needle, $offset);
9572
            if ($return_tmp !== false) {
9573
                return $return_tmp;
9574
            }
9575
        }
9576
9577
        //
9578
        // fallback for ascii only
9579
        //
9580
9581
        if (ASCII::is_ascii($haystack . $needle)) {
9582
            return \strripos($haystack, $needle, $offset);
9583
        }
9584
9585
        //
9586
        // fallback via vanilla php
9587
        //
9588
9589
        $haystack = self::strtocasefold($haystack, true, false, $encoding);
9590
        $needle = self::strtocasefold($needle, true, false, $encoding);
9591
9592
        return self::strrpos($haystack, $needle, $offset, $encoding, $clean_utf8);
9593
    }
9594
9595
    /**
9596
     * Finds position of last occurrence of a string within another, case-insensitive.
9597
     *
9598
     * @param string $haystack <p>
9599
     *                         The string from which to get the position of the last occurrence
9600
     *                         of needle.
9601
     *                         </p>
9602
     * @param string $needle   <p>
9603
     *                         The string to find in haystack.
9604
     *                         </p>
9605
     * @param int    $offset   [optional] <p>
9606
     *                         The position in haystack
9607
     *                         to start searching.
9608
     *                         </p>
9609
     *
9610
     * @return false|int
9611
     *                   <p>eturn the numeric position of the last occurrence of needle in the
9612
     *                   haystack string, or false if needle is not found.</p>
9613
     */
9614
    public static function strripos_in_byte(string $haystack, string $needle, int $offset = 0)
9615
    {
9616
        if ($haystack === '' || $needle === '') {
9617
            return false;
9618
        }
9619
9620
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9621
            // "mb_" is available if overload is used, so use it ...
9622
            return \mb_strripos($haystack, $needle, $offset, 'CP850'); // 8-BIT
9623
        }
9624
9625
        return \strripos($haystack, $needle, $offset);
9626
    }
9627
9628
    /**
9629
     * Find the position of the last occurrence of a substring in a string.
9630
     *
9631
     * @see http://php.net/manual/en/function.mb-strrpos.php
9632
     *
9633
     * @param string     $haystack   <p>The string being checked, for the last occurrence of needle</p>
9634
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
9635
     * @param int        $offset     [optional] <p>May be specified to begin searching an arbitrary number of characters
9636
     *                               into the string. Negative values will stop searching at an arbitrary point prior to
9637
     *                               the end of the string.
9638
     *                               </p>
9639
     * @param string     $encoding   [optional] <p>Set the charset.</p>
9640
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9641
     *
9642
     * @return false|int
9643
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
9644
     *                   string.<br>If needle is not found, it returns false.</p>
9645
     */
9646
    public static function strrpos(
9647
        string $haystack,
9648
        $needle,
9649
        int $offset = 0,
9650
        string $encoding = 'UTF-8',
9651
        bool $clean_utf8 = false
9652
    ) {
9653 35
        if ($haystack === '') {
9654 3
            return false;
9655
        }
9656
9657
        // iconv and mbstring do not support integer $needle
9658 34
        if ((int) $needle === $needle && $needle >= 0) {
9659 2
            $needle = (string) self::chr($needle);
9660
        }
9661 34
        $needle = (string) $needle;
9662
9663 34
        if ($needle === '') {
9664 2
            return false;
9665
        }
9666
9667 34
        if ($clean_utf8 === true) {
9668
            // mb_strrpos && iconv_strrpos is not tolerant to invalid characters
9669 4
            $needle = self::clean($needle);
9670 4
            $haystack = self::clean($haystack);
9671
        }
9672
9673 34
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9674 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9675
        }
9676
9677
        //
9678
        // fallback via mbstring
9679
        //
9680
9681 34
        if (self::$SUPPORT['mbstring'] === true) {
9682 34
            if ($encoding === 'UTF-8') {
9683 34
                return \mb_strrpos($haystack, $needle, $offset);
9684
            }
9685
9686 2
            return \mb_strrpos($haystack, $needle, $offset, $encoding);
9687
        }
9688
9689
        //
9690
        // fallback for binary || ascii only
9691
        //
9692
9693
        if (
9694
            $encoding === 'CP850'
9695
            ||
9696
            $encoding === 'ASCII'
9697
        ) {
9698
            return \strrpos($haystack, $needle, $offset);
9699
        }
9700
9701
        if (
9702
            $encoding !== 'UTF-8'
9703
            &&
9704
            self::$SUPPORT['mbstring'] === false
9705
        ) {
9706
            \trigger_error('UTF8::strrpos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9707
        }
9708
9709
        //
9710
        // fallback via intl
9711
        //
9712
9713
        if (
9714
            $offset >= 0 // grapheme_strrpos() can't handle negative offset
9715
            &&
9716
            $encoding === 'UTF-8' // INFO: "grapheme_strrpos()" can't handle other encodings
9717
            &&
9718
            self::$SUPPORT['intl'] === true
9719
        ) {
9720
            $return_tmp = \grapheme_strrpos($haystack, $needle, $offset);
9721
            if ($return_tmp !== false) {
9722
                return $return_tmp;
9723
            }
9724
        }
9725
9726
        //
9727
        // fallback for ascii only
9728
        //
9729
9730
        if (ASCII::is_ascii($haystack . $needle)) {
9731
            return \strrpos($haystack, $needle, $offset);
9732
        }
9733
9734
        //
9735
        // fallback via vanilla php
9736
        //
9737
9738
        $haystack_tmp = null;
9739
        if ($offset > 0) {
9740
            $haystack_tmp = self::substr($haystack, $offset);
9741
        } elseif ($offset < 0) {
9742
            $haystack_tmp = self::substr($haystack, 0, $offset);
9743
            $offset = 0;
9744
        }
9745
9746
        if ($haystack_tmp !== null) {
9747
            if ($haystack_tmp === false) {
9748
                $haystack_tmp = '';
9749
            }
9750
            $haystack = (string) $haystack_tmp;
9751
        }
9752
9753
        $pos = \strrpos($haystack, $needle);
9754
        if ($pos === false) {
9755
            return false;
9756
        }
9757
9758
        /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
9759
        $str_tmp = \substr($haystack, 0, $pos);
9760
        if ($str_tmp === false) {
9761
            return false;
9762
        }
9763
9764
        return $offset + (int) self::strlen($str_tmp);
9765
    }
9766
9767
    /**
9768
     * Find the position of the last occurrence of a substring in a string.
9769
     *
9770
     * @param string $haystack <p>
9771
     *                         The string being checked, for the last occurrence
9772
     *                         of needle.
9773
     *                         </p>
9774
     * @param string $needle   <p>
9775
     *                         The string to find in haystack.
9776
     *                         </p>
9777
     * @param int    $offset   [optional] <p>May be specified to begin searching an arbitrary number of characters into
9778
     *                         the string. Negative values will stop searching at an arbitrary point
9779
     *                         prior to the end of the string.
9780
     *                         </p>
9781
     *
9782
     * @return false|int
9783
     *                   <p>The numeric position of the last occurrence of needle in the
9784
     *                   haystack string. If needle is not found, it returns false.</p>
9785
     */
9786
    public static function strrpos_in_byte(string $haystack, string $needle, int $offset = 0)
9787
    {
9788
        if ($haystack === '' || $needle === '') {
9789
            return false;
9790
        }
9791
9792
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9793
            // "mb_" is available if overload is used, so use it ...
9794
            return \mb_strrpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
9795
        }
9796
9797
        return \strrpos($haystack, $needle, $offset);
9798
    }
9799
9800
    /**
9801
     * Finds the length of the initial segment of a string consisting entirely of characters contained within a given
9802
     * mask.
9803
     *
9804
     * @param string $str      <p>The input string.</p>
9805
     * @param string $mask     <p>The mask of chars</p>
9806
     * @param int    $offset   [optional]
9807
     * @param int    $length   [optional]
9808
     * @param string $encoding [optional] <p>Set the charset.</p>
9809
     *
9810
     * @return false|int
9811
     */
9812
    public static function strspn(
9813
        string $str,
9814
        string $mask,
9815
        int $offset = 0,
9816
        int $length = null,
9817
        string $encoding = 'UTF-8'
9818
    ) {
9819 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9820
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9821
        }
9822
9823 10
        if ($offset || $length !== null) {
9824 2
            if ($encoding === 'UTF-8') {
9825 2
                if ($length === null) {
9826
                    $str = (string) \mb_substr($str, $offset);
9827
                } else {
9828 2
                    $str = (string) \mb_substr($str, $offset, $length);
9829
                }
9830
            } else {
9831
                $str = (string) self::substr($str, $offset, $length, $encoding);
9832
            }
9833
        }
9834
9835 10
        if ($str === '' || $mask === '') {
9836 2
            return 0;
9837
        }
9838
9839 8
        $matches = [];
9840
9841 8
        return \preg_match('/^' . self::rxClass($mask) . '+/u', $str, $matches) ? (int) self::strlen($matches[0], $encoding) : 0;
9842
    }
9843
9844
    /**
9845
     * Returns part of haystack string from the first occurrence of needle to the end of haystack.
9846
     *
9847
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
9848
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
9849
     * @param bool   $before_needle [optional] <p>
9850
     *                              If <b>TRUE</b>, strstr() returns the part of the
9851
     *                              haystack before the first occurrence of the needle (excluding the needle).
9852
     *                              </p>
9853
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9854
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9855
     *
9856
     * @return false|string
9857
     *                      A sub-string,<br>or <strong>false</strong> if needle is not found
9858
     */
9859
    public static function strstr(
9860
        string $haystack,
9861
        string $needle,
9862
        bool $before_needle = false,
9863
        string $encoding = 'UTF-8',
9864
        $clean_utf8 = false
9865
    ) {
9866 3
        if ($haystack === '' || $needle === '') {
9867 2
            return false;
9868
        }
9869
9870 3
        if ($clean_utf8 === true) {
9871
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9872
            // if invalid characters are found in $haystack before $needle
9873
            $needle = self::clean($needle);
9874
            $haystack = self::clean($haystack);
9875
        }
9876
9877 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9878 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9879
        }
9880
9881
        //
9882
        // fallback via mbstring
9883
        //
9884
9885 3
        if (self::$SUPPORT['mbstring'] === true) {
9886 3
            if ($encoding === 'UTF-8') {
9887 3
                return \mb_strstr($haystack, $needle, $before_needle);
9888
            }
9889
9890 2
            return \mb_strstr($haystack, $needle, $before_needle, $encoding);
9891
        }
9892
9893
        //
9894
        // fallback for binary || ascii only
9895
        //
9896
9897
        if (
9898
            $encoding === 'CP850'
9899
            ||
9900
            $encoding === 'ASCII'
9901
        ) {
9902
            return \strstr($haystack, $needle, $before_needle);
9903
        }
9904
9905
        if (
9906
            $encoding !== 'UTF-8'
9907
            &&
9908
            self::$SUPPORT['mbstring'] === false
9909
        ) {
9910
            \trigger_error('UTF8::strstr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9911
        }
9912
9913
        //
9914
        // fallback via intl
9915
        //
9916
9917
        if (
9918
            $encoding === 'UTF-8' // INFO: "grapheme_strstr()" can't handle other encodings
9919
            &&
9920
            self::$SUPPORT['intl'] === true
9921
        ) {
9922
            $return_tmp = \grapheme_strstr($haystack, $needle, $before_needle);
9923
            if ($return_tmp !== false) {
9924
                return $return_tmp;
9925
            }
9926
        }
9927
9928
        //
9929
        // fallback for ascii only
9930
        //
9931
9932
        if (ASCII::is_ascii($haystack . $needle)) {
9933
            return \strstr($haystack, $needle, $before_needle);
9934
        }
9935
9936
        //
9937
        // fallback via vanilla php
9938
        //
9939
9940
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/us', $haystack, $match);
9941
9942
        if (!isset($match[1])) {
9943
            return false;
9944
        }
9945
9946
        if ($before_needle) {
9947
            return $match[1];
9948
        }
9949
9950
        return self::substr($haystack, (int) self::strlen($match[1]));
9951
    }
9952
9953
    /**
9954
     *  * Finds first occurrence of a string within another.
9955
     *
9956
     * @param string $haystack      <p>
9957
     *                              The string from which to get the first occurrence
9958
     *                              of needle.
9959
     *                              </p>
9960
     * @param string $needle        <p>
9961
     *                              The string to find in haystack.
9962
     *                              </p>
9963
     * @param bool   $before_needle [optional] <p>
9964
     *                              Determines which portion of haystack
9965
     *                              this function returns.
9966
     *                              If set to true, it returns all of haystack
9967
     *                              from the beginning to the first occurrence of needle.
9968
     *                              If set to false, it returns all of haystack
9969
     *                              from the first occurrence of needle to the end,
9970
     *                              </p>
9971
     *
9972
     * @return false|string
9973
     *                      <p>The portion of haystack,
9974
     *                      or false if needle is not found.</p>
9975
     */
9976
    public static function strstr_in_byte(
9977
        string $haystack,
9978
        string $needle,
9979
        bool $before_needle = false
9980
    ) {
9981
        if ($haystack === '' || $needle === '') {
9982
            return false;
9983
        }
9984
9985
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9986
            // "mb_" is available if overload is used, so use it ...
9987
            return \mb_strstr($haystack, $needle, $before_needle, 'CP850'); // 8-BIT
9988
        }
9989
9990
        return \strstr($haystack, $needle, $before_needle);
9991
    }
9992
9993
    /**
9994
     * Unicode transformation for case-less matching.
9995
     *
9996
     * @see http://unicode.org/reports/tr21/tr21-5.html
9997
     *
9998
     * @param string      $str        <p>The input string.</p>
9999
     * @param bool        $full       [optional] <p>
10000
     *                                <b>true</b>, replace full case folding chars (default)<br>
10001
     *                                <b>false</b>, use only limited static array [UTF8::$COMMON_CASE_FOLD]
10002
     *                                </p>
10003
     * @param bool        $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10004
     * @param string      $encoding   [optional] <p>Set the charset.</p>
10005
     * @param string|null $lang       [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10006
     * @param bool        $lower      [optional] <p>Use lowercase string, otherwise use uppercase string. PS: uppercase
10007
     *                                is for some languages better ...</p>
10008
     *
10009
     * @return string
10010
     */
10011
    public static function strtocasefold(
10012
        string $str,
10013
        bool $full = true,
10014
        bool $clean_utf8 = false,
10015
        string $encoding = 'UTF-8',
10016
        string $lang = null,
10017
        $lower = true
10018
    ): string {
10019 32
        if ($str === '') {
10020 5
            return '';
10021
        }
10022
10023 31
        if ($clean_utf8 === true) {
10024
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10025
            // if invalid characters are found in $haystack before $needle
10026 2
            $str = self::clean($str);
10027
        }
10028
10029 31
        $str = self::fixStrCaseHelper($str, $lower, $full);
10030
10031 31
        if ($lang === null && $encoding === 'UTF-8') {
10032 31
            if ($lower === true) {
10033 2
                return \mb_strtolower($str);
10034
            }
10035
10036 29
            return \mb_strtoupper($str);
10037
        }
10038
10039 2
        if ($lower === true) {
10040
            return self::strtolower($str, $encoding, false, $lang);
10041
        }
10042
10043 2
        return self::strtoupper($str, $encoding, false, $lang);
10044
    }
10045
10046
    /**
10047
     * Make a string lowercase.
10048
     *
10049
     * @see http://php.net/manual/en/function.mb-strtolower.php
10050
     *
10051
     * @param string      $str                           <p>The string being lowercased.</p>
10052
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
10053
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
10054
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10055
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
10056
     *
10057
     * @return string
10058
     *                <p>String with all alphabetic characters converted to lowercase.</p>
10059
     */
10060
    public static function strtolower(
10061
        $str,
10062
        string $encoding = 'UTF-8',
10063
        bool $clean_utf8 = false,
10064
        string $lang = null,
10065
        bool $try_to_keep_the_string_length = false
10066
    ): string {
10067
        // init
10068 73
        $str = (string) $str;
10069
10070 73
        if ($str === '') {
10071 1
            return '';
10072
        }
10073
10074 72
        if ($clean_utf8 === true) {
10075
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10076
            // if invalid characters are found in $haystack before $needle
10077 2
            $str = self::clean($str);
10078
        }
10079
10080
        // hack for old php version or for the polyfill ...
10081 72
        if ($try_to_keep_the_string_length === true) {
10082
            $str = self::fixStrCaseHelper($str, true);
10083
        }
10084
10085 72
        if ($lang === null && $encoding === 'UTF-8') {
10086 13
            return \mb_strtolower($str);
10087
        }
10088
10089 61
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10090
10091 61
        if ($lang !== null) {
10092 2
            if (self::$SUPPORT['intl'] === true) {
10093 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
10094
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
10095
                }
10096
10097 2
                $language_code = $lang . '-Lower';
10098 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
10099
                    \trigger_error('UTF8::strtolower() cannot handle special language: ' . $lang . ' | supported: ' . \print_r(self::$INTL_TRANSLITERATOR_LIST, true), \E_USER_WARNING);
10100
10101
                    $language_code = 'Any-Lower';
10102
                }
10103
10104
                /** @noinspection PhpComposerExtensionStubsInspection */
10105
                /** @noinspection UnnecessaryCastingInspection */
10106 2
                return (string) \transliterator_transliterate($language_code, $str);
10107
            }
10108
10109
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang" parameter: ' . $lang, \E_USER_WARNING);
10110
        }
10111
10112
        // always fallback via symfony polyfill
10113 61
        return \mb_strtolower($str, $encoding);
10114
    }
10115
10116
    /**
10117
     * Make a string uppercase.
10118
     *
10119
     * @see http://php.net/manual/en/function.mb-strtoupper.php
10120
     *
10121
     * @param string      $str                           <p>The string being uppercased.</p>
10122
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
10123
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
10124
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10125
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
10126
     *
10127
     * @return string
10128
     *                <p>String with all alphabetic characters converted to uppercase.</p>
10129
     */
10130
    public static function strtoupper(
10131
        $str,
10132
        string $encoding = 'UTF-8',
10133
        bool $clean_utf8 = false,
10134
        string $lang = null,
10135
        bool $try_to_keep_the_string_length = false
10136
    ): string {
10137
        // init
10138 17
        $str = (string) $str;
10139
10140 17
        if ($str === '') {
10141 1
            return '';
10142
        }
10143
10144 16
        if ($clean_utf8 === true) {
10145
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10146
            // if invalid characters are found in $haystack before $needle
10147 2
            $str = self::clean($str);
10148
        }
10149
10150
        // hack for old php version or for the polyfill ...
10151 16
        if ($try_to_keep_the_string_length === true) {
10152 2
            $str = self::fixStrCaseHelper($str, false);
10153
        }
10154
10155 16
        if ($lang === null && $encoding === 'UTF-8') {
10156 8
            return \mb_strtoupper($str);
10157
        }
10158
10159 10
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10160
10161 10
        if ($lang !== null) {
10162 2
            if (self::$SUPPORT['intl'] === true) {
10163 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
10164
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
10165
                }
10166
10167 2
                $language_code = $lang . '-Upper';
10168 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
10169
                    \trigger_error('UTF8::strtoupper() without intl for special language: ' . $lang, \E_USER_WARNING);
10170
10171
                    $language_code = 'Any-Upper';
10172
                }
10173
10174
                /** @noinspection PhpComposerExtensionStubsInspection */
10175
                /** @noinspection UnnecessaryCastingInspection */
10176 2
                return (string) \transliterator_transliterate($language_code, $str);
10177
            }
10178
10179
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang"-parameter: ' . $lang, \E_USER_WARNING);
10180
        }
10181
10182
        // always fallback via symfony polyfill
10183 10
        return \mb_strtoupper($str, $encoding);
10184
    }
10185
10186
    /**
10187
     * Translate characters or replace sub-strings.
10188
     *
10189
     * @see http://php.net/manual/en/function.strtr.php
10190
     *
10191
     * @param string          $str  <p>The string being translated.</p>
10192
     * @param string|string[] $from <p>The string replacing from.</p>
10193
     * @param string|string[] $to   [optional] <p>The string being translated to to.</p>
10194
     *
10195
     * @return string
10196
     *                <p>This function returns a copy of str, translating all occurrences of each character in "from" to the
10197
     *                corresponding character in "to".</p>
10198
     */
10199
    public static function strtr(string $str, $from, $to = ''): string
10200
    {
10201 2
        if ($str === '') {
10202
            return '';
10203
        }
10204
10205 2
        if ($from === $to) {
10206
            return $str;
10207
        }
10208
10209 2
        if ($to !== '') {
10210 2
            $from = self::str_split($from);
10211 2
            $to = self::str_split($to);
10212 2
            $count_from = \count($from);
10213 2
            $count_to = \count($to);
10214
10215 2
            if ($count_from > $count_to) {
10216 2
                $from = \array_slice($from, 0, $count_to);
10217 2
            } elseif ($count_from < $count_to) {
10218 2
                $to = \array_slice($to, 0, $count_from);
10219
            }
10220
10221 2
            $from = \array_combine($from, $to);
10222
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
10223 2
            if ($from === false) {
10224
                throw new \InvalidArgumentException('The number of elements for each array isn\'t equal or the arrays are empty: (from: ' . \print_r($from, true) . ' | to: ' . \print_r($to, true) . ')');
10225
            }
10226
        }
10227
10228 2
        if (\is_string($from)) {
10229 2
            return \str_replace($from, '', $str);
10230
        }
10231
10232 2
        return \strtr($str, $from);
10233
    }
10234
10235
    /**
10236
     * Return the width of a string.
10237
     *
10238
     * @param string $str        <p>The input string.</p>
10239
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10240
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10241
     *
10242
     * @return int
10243
     */
10244
    public static function strwidth(
10245
        string $str,
10246
        string $encoding = 'UTF-8',
10247
        bool $clean_utf8 = false
10248
    ): int {
10249 2
        if ($str === '') {
10250 2
            return 0;
10251
        }
10252
10253 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10254 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10255
        }
10256
10257 2
        if ($clean_utf8 === true) {
10258
            // iconv and mbstring are not tolerant to invalid encoding
10259
            // further, their behaviour is inconsistent with that of PHP's substr
10260 2
            $str = self::clean($str);
10261
        }
10262
10263
        //
10264
        // fallback via mbstring
10265
        //
10266
10267 2
        if (self::$SUPPORT['mbstring'] === true) {
10268 2
            if ($encoding === 'UTF-8') {
10269 2
                return \mb_strwidth($str);
10270
            }
10271
10272
            return \mb_strwidth($str, $encoding);
10273
        }
10274
10275
        //
10276
        // fallback via vanilla php
10277
        //
10278
10279
        if ($encoding !== 'UTF-8') {
10280
            $str = self::encode('UTF-8', $str, false, $encoding);
10281
        }
10282
10283
        $wide = 0;
10284
        $str = (string) \preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $str, -1, $wide);
10285
10286
        return ($wide << 1) + (int) self::strlen($str, 'UTF-8');
10287
    }
10288
10289
    /**
10290
     * Get part of a string.
10291
     *
10292
     * @see http://php.net/manual/en/function.mb-substr.php
10293
     *
10294
     * @param string $str        <p>The string being checked.</p>
10295
     * @param int    $offset     <p>The first position used in str.</p>
10296
     * @param int    $length     [optional] <p>The maximum length of the returned string.</p>
10297
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10298
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10299
     *
10300
     * @return false|string
10301
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
10302
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
10303
     *                      characters long, <b>FALSE</b> will be returned.
10304
     */
10305
    public static function substr(
10306
        string $str,
10307
        int $offset = 0,
10308
        int $length = null,
10309
        string $encoding = 'UTF-8',
10310
        bool $clean_utf8 = false
10311
    ) {
10312
        // empty string
10313 172
        if ($str === '' || $length === 0) {
10314 8
            return '';
10315
        }
10316
10317 168
        if ($clean_utf8 === true) {
10318
            // iconv and mbstring are not tolerant to invalid encoding
10319
            // further, their behaviour is inconsistent with that of PHP's substr
10320 2
            $str = self::clean($str);
10321
        }
10322
10323
        // whole string
10324 168
        if (!$offset && $length === null) {
10325 7
            return $str;
10326
        }
10327
10328 163
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10329 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10330
        }
10331
10332
        //
10333
        // fallback via mbstring
10334
        //
10335
10336 163
        if (self::$SUPPORT['mbstring'] === true) {
10337 161
            if ($encoding === 'UTF-8') {
10338 161
                if ($length === null) {
10339 64
                    return \mb_substr($str, $offset);
10340
                }
10341
10342 102
                return \mb_substr($str, $offset, $length);
10343
            }
10344
10345
            return self::substr($str, $offset, $length, $encoding);
10346
        }
10347
10348
        //
10349
        // fallback for binary || ascii only
10350
        //
10351
10352
        if (
10353 4
            $encoding === 'CP850'
10354
            ||
10355 4
            $encoding === 'ASCII'
10356
        ) {
10357
            if ($length === null) {
10358
                return \substr($str, $offset);
10359
            }
10360
10361
            return \substr($str, $offset, $length);
10362
        }
10363
10364
        // otherwise we need the string-length
10365 4
        $str_length = 0;
10366 4
        if ($offset || $length === null) {
10367 4
            $str_length = self::strlen($str, $encoding);
10368
        }
10369
10370
        // e.g.: invalid chars + mbstring not installed
10371 4
        if ($str_length === false) {
10372
            return false;
10373
        }
10374
10375
        // empty string
10376 4
        if ($offset === $str_length && !$length) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $length of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
10377
            return '';
10378
        }
10379
10380
        // impossible
10381 4
        if ($offset && $offset > $str_length) {
10382
            return '';
10383
        }
10384
10385 4
        if ($length === null) {
10386 4
            $length = (int) $str_length;
10387
        } else {
10388 2
            $length = (int) $length;
10389
        }
10390
10391
        if (
10392 4
            $encoding !== 'UTF-8'
10393
            &&
10394 4
            self::$SUPPORT['mbstring'] === false
10395
        ) {
10396 2
            \trigger_error('UTF8::substr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10397
        }
10398
10399
        //
10400
        // fallback via intl
10401
        //
10402
10403
        if (
10404 4
            $encoding === 'UTF-8' // INFO: "grapheme_substr()" can't handle other encodings
10405
            &&
10406 4
            $offset >= 0 // grapheme_substr() can't handle negative offset
10407
            &&
10408 4
            self::$SUPPORT['intl'] === true
10409
        ) {
10410
            $return_tmp = \grapheme_substr($str, $offset, $length);
10411
            if ($return_tmp !== false) {
10412
                return $return_tmp;
10413
            }
10414
        }
10415
10416
        //
10417
        // fallback via iconv
10418
        //
10419
10420
        if (
10421 4
            $length >= 0 // "iconv_substr()" can't handle negative length
10422
            &&
10423 4
            self::$SUPPORT['iconv'] === true
10424
        ) {
10425
            $return_tmp = \iconv_substr($str, $offset, $length);
10426
            if ($return_tmp !== false) {
10427
                return $return_tmp;
10428
            }
10429
        }
10430
10431
        //
10432
        // fallback for ascii only
10433
        //
10434
10435 4
        if (ASCII::is_ascii($str)) {
10436
            return \substr($str, $offset, $length);
10437
        }
10438
10439
        //
10440
        // fallback via vanilla php
10441
        //
10442
10443
        // split to array, and remove invalid characters
10444 4
        $array = self::str_split($str);
10445
10446
        // extract relevant part, and join to make sting again
10447 4
        return \implode('', \array_slice($array, $offset, $length));
10448
    }
10449
10450
    /**
10451
     * Binary-safe comparison of two strings from an offset, up to a length of characters.
10452
     *
10453
     * @param string   $str1               <p>The main string being compared.</p>
10454
     * @param string   $str2               <p>The secondary string being compared.</p>
10455
     * @param int      $offset             [optional] <p>The start position for the comparison. If negative, it starts
10456
     *                                     counting from the end of the string.</p>
10457
     * @param int|null $length             [optional] <p>The length of the comparison. The default value is the largest
10458
     *                                     of the length of the str compared to the length of main_str less the
10459
     *                                     offset.</p>
10460
     * @param bool     $case_insensitivity [optional] <p>If case_insensitivity is TRUE, comparison is case
10461
     *                                     insensitive.</p>
10462
     * @param string   $encoding           [optional] <p>Set the charset for e.g. "mb_" function</p>
10463
     *
10464
     * @return int
10465
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
10466
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
10467
     *             <strong>0</strong> if they are equal
10468
     */
10469
    public static function substr_compare(
10470
        string $str1,
10471
        string $str2,
10472
        int $offset = 0,
10473
        int $length = null,
10474
        bool $case_insensitivity = false,
10475
        string $encoding = 'UTF-8'
10476
    ): int {
10477
        if (
10478 2
            $offset !== 0
10479
            ||
10480 2
            $length !== null
10481
        ) {
10482 2
            if ($encoding === 'UTF-8') {
10483 2
                if ($length === null) {
10484 2
                    $str1 = (string) \mb_substr($str1, $offset);
10485
                } else {
10486 2
                    $str1 = (string) \mb_substr($str1, $offset, $length);
10487
                }
10488 2
                $str2 = (string) \mb_substr($str2, 0, (int) self::strlen($str1));
10489
            } else {
10490
                $encoding = self::normalize_encoding($encoding, 'UTF-8');
10491
10492
                $str1 = (string) self::substr($str1, $offset, $length, $encoding);
10493
                $str2 = (string) self::substr($str2, 0, (int) self::strlen($str1), $encoding);
10494
            }
10495
        }
10496
10497 2
        if ($case_insensitivity === true) {
10498 2
            return self::strcasecmp($str1, $str2, $encoding);
10499
        }
10500
10501 2
        return self::strcmp($str1, $str2);
10502
    }
10503
10504
    /**
10505
     * Count the number of substring occurrences.
10506
     *
10507
     * @see http://php.net/manual/en/function.substr-count.php
10508
     *
10509
     * @param string $haystack   <p>The string to search in.</p>
10510
     * @param string $needle     <p>The substring to search for.</p>
10511
     * @param int    $offset     [optional] <p>The offset where to start counting.</p>
10512
     * @param int    $length     [optional] <p>
10513
     *                           The maximum length after the specified offset to search for the
10514
     *                           substring. It outputs a warning if the offset plus the length is
10515
     *                           greater than the haystack length.
10516
     *                           </p>
10517
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10518
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10519
     *
10520
     * @return false|int this functions returns an integer or false if there isn't a string
10521
     */
10522
    public static function substr_count(
10523
        string $haystack,
10524
        string $needle,
10525
        int $offset = 0,
10526
        int $length = null,
10527
        string $encoding = 'UTF-8',
10528
        bool $clean_utf8 = false
10529
    ) {
10530 5
        if ($haystack === '' || $needle === '') {
10531 2
            return false;
10532
        }
10533
10534 5
        if ($length === 0) {
10535 2
            return 0;
10536
        }
10537
10538 5
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10539 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10540
        }
10541
10542 5
        if ($clean_utf8 === true) {
10543
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10544
            // if invalid characters are found in $haystack before $needle
10545
            $needle = self::clean($needle);
10546
            $haystack = self::clean($haystack);
10547
        }
10548
10549 5
        if ($offset || $length > 0) {
10550 2
            if ($length === null) {
10551 2
                $length_tmp = self::strlen($haystack, $encoding);
10552 2
                if ($length_tmp === false) {
10553
                    return false;
10554
                }
10555 2
                $length = (int) $length_tmp;
10556
            }
10557
10558 2
            if ($encoding === 'UTF-8') {
10559 2
                $haystack = (string) \mb_substr($haystack, $offset, $length);
10560
            } else {
10561 2
                $haystack = (string) \mb_substr($haystack, $offset, $length, $encoding);
10562
            }
10563
        }
10564
10565
        if (
10566 5
            $encoding !== 'UTF-8'
10567
            &&
10568 5
            self::$SUPPORT['mbstring'] === false
10569
        ) {
10570
            \trigger_error('UTF8::substr_count() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10571
        }
10572
10573 5
        if (self::$SUPPORT['mbstring'] === true) {
10574 5
            if ($encoding === 'UTF-8') {
10575 5
                return \mb_substr_count($haystack, $needle);
10576
            }
10577
10578 2
            return \mb_substr_count($haystack, $needle, $encoding);
10579
        }
10580
10581
        \preg_match_all('/' . \preg_quote($needle, '/') . '/us', $haystack, $matches, \PREG_SET_ORDER);
10582
10583
        return \count($matches);
10584
    }
10585
10586
    /**
10587
     * Count the number of substring occurrences.
10588
     *
10589
     * @param string $haystack <p>
10590
     *                         The string being checked.
10591
     *                         </p>
10592
     * @param string $needle   <p>
10593
     *                         The string being found.
10594
     *                         </p>
10595
     * @param int    $offset   [optional] <p>
10596
     *                         The offset where to start counting
10597
     *                         </p>
10598
     * @param int    $length   [optional] <p>
10599
     *                         The maximum length after the specified offset to search for the
10600
     *                         substring. It outputs a warning if the offset plus the length is
10601
     *                         greater than the haystack length.
10602
     *                         </p>
10603
     *
10604
     * @return false|int the number of times the
10605
     *                   needle substring occurs in the
10606
     *                   haystack string
10607
     */
10608
    public static function substr_count_in_byte(
10609
        string $haystack,
10610
        string $needle,
10611
        int $offset = 0,
10612
        int $length = null
10613
    ) {
10614
        if ($haystack === '' || $needle === '') {
10615
            return 0;
10616
        }
10617
10618
        if (
10619
            ($offset || $length !== null)
10620
            &&
10621
            self::$SUPPORT['mbstring_func_overload'] === true
10622
        ) {
10623
            if ($length === null) {
10624
                $length_tmp = self::strlen($haystack);
10625
                if ($length_tmp === false) {
10626
                    return false;
10627
                }
10628
                $length = (int) $length_tmp;
10629
            }
10630
10631
            if (
10632
                (
10633
                    $length !== 0
10634
                    &&
10635
                    $offset !== 0
10636
                )
10637
                &&
10638
                ($length + $offset) <= 0
10639
                &&
10640
                Bootup::is_php('7.1') === false // output from "substr_count()" have changed in PHP 7.1
10641
            ) {
10642
                return false;
10643
            }
10644
10645
            /** @var false|string $haystack_tmp - needed for PhpStan (stubs error) */
10646
            $haystack_tmp = \substr($haystack, $offset, $length);
10647
            if ($haystack_tmp === false) {
10648
                $haystack_tmp = '';
10649
            }
10650
            $haystack = (string) $haystack_tmp;
10651
        }
10652
10653
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10654
            // "mb_" is available if overload is used, so use it ...
10655
            return \mb_substr_count($haystack, $needle, 'CP850'); // 8-BIT
10656
        }
10657
10658
        if ($length === null) {
10659
            return \substr_count($haystack, $needle, $offset);
10660
        }
10661
10662
        return \substr_count($haystack, $needle, $offset, $length);
10663
    }
10664
10665
    /**
10666
     * Returns the number of occurrences of $substring in the given string.
10667
     * By default, the comparison is case-sensitive, but can be made insensitive
10668
     * by setting $case_sensitive to false.
10669
     *
10670
     * @param string $str            <p>The input string.</p>
10671
     * @param string $substring      <p>The substring to search for.</p>
10672
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
10673
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
10674
     *
10675
     * @return int
10676
     */
10677
    public static function substr_count_simple(
10678
        string $str,
10679
        string $substring,
10680
        bool $case_sensitive = true,
10681
        string $encoding = 'UTF-8'
10682
    ): int {
10683 15
        if ($str === '' || $substring === '') {
10684 2
            return 0;
10685
        }
10686
10687 13
        if ($encoding === 'UTF-8') {
10688 7
            if ($case_sensitive) {
10689
                return (int) \mb_substr_count($str, $substring);
10690
            }
10691
10692 7
            return (int) \mb_substr_count(
10693 7
                \mb_strtoupper($str),
10694 7
                \mb_strtoupper($substring)
10695
            );
10696
        }
10697
10698 6
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10699
10700 6
        if ($case_sensitive) {
10701 3
            return (int) \mb_substr_count($str, $substring, $encoding);
10702
        }
10703
10704 3
        return (int) \mb_substr_count(
10705 3
            self::strtocasefold($str, true, false, $encoding, null, false),
10706 3
            self::strtocasefold($substring, true, false, $encoding, null, false),
10707 3
            $encoding
10708
        );
10709
    }
10710
10711
    /**
10712
     * Removes a prefix ($needle) from the beginning of the string ($haystack), case-insensitive.
10713
     *
10714
     * @param string $haystack <p>The string to search in.</p>
10715
     * @param string $needle   <p>The substring to search for.</p>
10716
     *
10717
     * @return string return the sub-string
10718
     */
10719
    public static function substr_ileft(string $haystack, string $needle): string
10720
    {
10721 2
        if ($haystack === '') {
10722 2
            return '';
10723
        }
10724
10725 2
        if ($needle === '') {
10726 2
            return $haystack;
10727
        }
10728
10729 2
        if (self::str_istarts_with($haystack, $needle) === true) {
10730 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
10731
        }
10732
10733 2
        return $haystack;
10734
    }
10735
10736
    /**
10737
     * Get part of a string process in bytes.
10738
     *
10739
     * @param string $str    <p>The string being checked.</p>
10740
     * @param int    $offset <p>The first position used in str.</p>
10741
     * @param int    $length [optional] <p>The maximum length of the returned string.</p>
10742
     *
10743
     * @return false|string
10744
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
10745
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
10746
     *                      characters long, <b>FALSE</b> will be returned.
10747
     */
10748
    public static function substr_in_byte(string $str, int $offset = 0, int $length = null)
10749
    {
10750
        // empty string
10751
        if ($str === '' || $length === 0) {
10752
            return '';
10753
        }
10754
10755
        // whole string
10756
        if (!$offset && $length === null) {
10757
            return $str;
10758
        }
10759
10760
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10761
            // "mb_" is available if overload is used, so use it ...
10762
            return \mb_substr($str, $offset, $length, 'CP850'); // 8-BIT
10763
        }
10764
10765
        return \substr($str, $offset, $length ?? 2147483647);
10766
    }
10767
10768
    /**
10769
     * Removes a suffix ($needle) from the end of the string ($haystack), case-insensitive.
10770
     *
10771
     * @param string $haystack <p>The string to search in.</p>
10772
     * @param string $needle   <p>The substring to search for.</p>
10773
     *
10774
     * @return string return the sub-string
10775
     */
10776
    public static function substr_iright(string $haystack, string $needle): string
10777
    {
10778 2
        if ($haystack === '') {
10779 2
            return '';
10780
        }
10781
10782 2
        if ($needle === '') {
10783 2
            return $haystack;
10784
        }
10785
10786 2
        if (self::str_iends_with($haystack, $needle) === true) {
10787 2
            $haystack = (string) \mb_substr($haystack, 0, (int) self::strlen($haystack) - (int) self::strlen($needle));
10788
        }
10789
10790 2
        return $haystack;
10791
    }
10792
10793
    /**
10794
     * Removes a prefix ($needle) from the beginning of the string ($haystack).
10795
     *
10796
     * @param string $haystack <p>The string to search in.</p>
10797
     * @param string $needle   <p>The substring to search for.</p>
10798
     *
10799
     * @return string return the sub-string
10800
     */
10801
    public static function substr_left(string $haystack, string $needle): string
10802
    {
10803 2
        if ($haystack === '') {
10804 2
            return '';
10805
        }
10806
10807 2
        if ($needle === '') {
10808 2
            return $haystack;
10809
        }
10810
10811 2
        if (self::str_starts_with($haystack, $needle) === true) {
10812 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
10813
        }
10814
10815 2
        return $haystack;
10816
    }
10817
10818
    /**
10819
     * Replace text within a portion of a string.
10820
     *
10821
     * source: https://gist.github.com/stemar/8287074
10822
     *
10823
     * @param string|string[] $str         <p>The input string or an array of stings.</p>
10824
     * @param string|string[] $replacement <p>The replacement string or an array of stings.</p>
10825
     * @param int|int[]       $offset      <p>
10826
     *                                     If start is positive, the replacing will begin at the start'th offset
10827
     *                                     into string.
10828
     *                                     <br><br>
10829
     *                                     If start is negative, the replacing will begin at the start'th character
10830
     *                                     from the end of string.
10831
     *                                     </p>
10832
     * @param int|int[]|null  $length      [optional] <p>If given and is positive, it represents the length of the
10833
     *                                     portion of string which is to be replaced. If it is negative, it
10834
     *                                     represents the number of characters from the end of string at which to
10835
     *                                     stop replacing. If it is not given, then it will default to strlen(
10836
     *                                     string ); i.e. end the replacing at the end of string. Of course, if
10837
     *                                     length is zero then this function will have the effect of inserting
10838
     *                                     replacement into string at the given start offset.</p>
10839
     * @param string          $encoding    [optional] <p>Set the charset for e.g. "mb_" function</p>
10840
     *
10841
     * @return string|string[] The result string is returned. If string is an array then array is returned.
10842
     */
10843
    public static function substr_replace(
10844
        $str,
10845
        $replacement,
10846
        $offset,
10847
        $length = null,
10848
        string $encoding = 'UTF-8'
10849
    ) {
10850 10
        if (\is_array($str) === true) {
10851 1
            $num = \count($str);
10852
10853
            // the replacement
10854 1
            if (\is_array($replacement) === true) {
10855 1
                $replacement = \array_slice($replacement, 0, $num);
10856
            } else {
10857 1
                $replacement = \array_pad([$replacement], $num, $replacement);
10858
            }
10859
10860
            // the offset
10861 1
            if (\is_array($offset) === true) {
10862 1
                $offset = \array_slice($offset, 0, $num);
10863 1
                foreach ($offset as &$value_tmp) {
10864 1
                    $value_tmp = (int) $value_tmp === $value_tmp ? $value_tmp : 0;
10865
                }
10866 1
                unset($value_tmp);
10867
            } else {
10868 1
                $offset = \array_pad([$offset], $num, $offset);
10869
            }
10870
10871
            // the length
10872 1
            if ($length === null) {
10873 1
                $length = \array_fill(0, $num, 0);
10874 1
            } elseif (\is_array($length) === true) {
10875 1
                $length = \array_slice($length, 0, $num);
10876 1
                foreach ($length as &$value_tmp_V2) {
10877 1
                    $value_tmp_V2 = (int) $value_tmp_V2 === $value_tmp_V2 ? $value_tmp_V2 : $num;
10878
                }
10879 1
                unset($value_tmp_V2);
10880
            } else {
10881 1
                $length = \array_pad([$length], $num, $length);
10882
            }
10883
10884
            // recursive call
10885 1
            return \array_map([self::class, 'substr_replace'], $str, $replacement, $offset, $length);
10886
        }
10887
10888 10
        if (\is_array($replacement) === true) {
10889 1
            if (\count($replacement) > 0) {
10890 1
                $replacement = $replacement[0];
10891
            } else {
10892 1
                $replacement = '';
10893
            }
10894
        }
10895
10896
        // init
10897 10
        $str = (string) $str;
10898 10
        $replacement = (string) $replacement;
10899
10900 10
        if (\is_array($length) === true) {
10901
            throw new \InvalidArgumentException('Parameter "$length" can only be an array, if "$str" is also an array.');
10902
        }
10903
10904 10
        if (\is_array($offset) === true) {
10905
            throw new \InvalidArgumentException('Parameter "$offset" can only be an array, if "$str" is also an array.');
10906
        }
10907
10908 10
        if ($str === '') {
10909 1
            return $replacement;
10910
        }
10911
10912 9
        if (self::$SUPPORT['mbstring'] === true) {
10913 9
            $string_length = (int) self::strlen($str, $encoding);
10914
10915 9
            if ($offset < 0) {
10916 1
                $offset = (int) \max(0, $string_length + $offset);
10917 9
            } elseif ($offset > $string_length) {
10918 1
                $offset = $string_length;
10919
            }
10920
10921 9
            if ($length !== null && $length < 0) {
10922 1
                $length = (int) \max(0, $string_length - $offset + $length);
10923 9
            } elseif ($length === null || $length > $string_length) {
10924 4
                $length = $string_length;
10925
            }
10926
10927
            /** @noinspection AdditionOperationOnArraysInspection */
10928 9
            if (($offset + $length) > $string_length) {
10929 4
                $length = $string_length - $offset;
10930
            }
10931
10932
            /** @noinspection AdditionOperationOnArraysInspection */
10933 9
            return ((string) \mb_substr($str, 0, $offset, $encoding)) .
10934 9
                   $replacement .
10935 9
                   ((string) \mb_substr($str, $offset + $length, $string_length - $offset - $length, $encoding));
10936
        }
10937
10938
        //
10939
        // fallback for ascii only
10940
        //
10941
10942
        if (ASCII::is_ascii($str)) {
10943
            return ($length === null) ?
10944
                \substr_replace($str, $replacement, $offset) :
10945
                \substr_replace($str, $replacement, $offset, $length);
10946
        }
10947
10948
        //
10949
        // fallback via vanilla php
10950
        //
10951
10952
        \preg_match_all('/./us', $str, $str_matches);
10953
        \preg_match_all('/./us', $replacement, $replacement_matches);
10954
10955
        if ($length === null) {
10956
            $length_tmp = self::strlen($str, $encoding);
10957
            if ($length_tmp === false) {
10958
                // e.g.: non mbstring support + invalid chars
10959
                return '';
10960
            }
10961
            $length = (int) $length_tmp;
10962
        }
10963
10964
        \array_splice($str_matches[0], $offset, $length, $replacement_matches[0]);
10965
10966
        return \implode('', $str_matches[0]);
10967
    }
10968
10969
    /**
10970
     * Removes a suffix ($needle) from the end of the string ($haystack).
10971
     *
10972
     * @param string $haystack <p>The string to search in.</p>
10973
     * @param string $needle   <p>The substring to search for.</p>
10974
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
10975
     *
10976
     * @return string return the sub-string
10977
     */
10978
    public static function substr_right(
10979
        string $haystack,
10980
        string $needle,
10981
        string $encoding = 'UTF-8'
10982
    ): string {
10983 2
        if ($haystack === '') {
10984 2
            return '';
10985
        }
10986
10987 2
        if ($needle === '') {
10988 2
            return $haystack;
10989
        }
10990
10991
        if (
10992 2
            $encoding === 'UTF-8'
10993
            &&
10994 2
            \substr($haystack, -\strlen($needle)) === $needle
10995
        ) {
10996 2
            return (string) \mb_substr($haystack, 0, (int) \mb_strlen($haystack) - (int) \mb_strlen($needle));
10997
        }
10998
10999 2
        if (\substr($haystack, -\strlen($needle)) === $needle) {
11000
            return (string) self::substr(
11001
                $haystack,
11002
                0,
11003
                (int) self::strlen($haystack, $encoding) - (int) self::strlen($needle, $encoding),
11004
                $encoding
11005
            );
11006
        }
11007
11008 2
        return $haystack;
11009
    }
11010
11011
    /**
11012
     * Returns a case swapped version of the string.
11013
     *
11014
     * @param string $str        <p>The input string.</p>
11015
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11016
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11017
     *
11018
     * @return string each character's case swapped
11019
     */
11020
    public static function swapCase(string $str, string $encoding = 'UTF-8', bool $clean_utf8 = false): string
11021
    {
11022 6
        if ($str === '') {
11023 1
            return '';
11024
        }
11025
11026 6
        if ($clean_utf8 === true) {
11027
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11028
            // if invalid characters are found in $haystack before $needle
11029 2
            $str = self::clean($str);
11030
        }
11031
11032 6
        if ($encoding === 'UTF-8') {
11033 4
            return (string) (\mb_strtolower($str) ^ \mb_strtoupper($str) ^ $str);
11034
        }
11035
11036 4
        return (string) (self::strtolower($str, $encoding) ^ self::strtoupper($str, $encoding) ^ $str);
11037
    }
11038
11039
    /**
11040
     * Checks whether symfony-polyfills are used.
11041
     *
11042
     * @return bool
11043
     *              <strong>true</strong> if in use, <strong>false</strong> otherwise
11044
     */
11045
    public static function symfony_polyfill_used(): bool
11046
    {
11047
        // init
11048
        $return = false;
11049
11050
        $return_tmp = \extension_loaded('mbstring');
11051
        if ($return_tmp === false && \function_exists('mb_strlen')) {
11052
            $return = true;
11053
        }
11054
11055
        $return_tmp = \extension_loaded('iconv');
11056
        if ($return_tmp === false && \function_exists('iconv')) {
11057
            $return = true;
11058
        }
11059
11060
        return $return;
11061
    }
11062
11063
    /**
11064
     * @param string $str
11065
     * @param int    $tab_length
11066
     *
11067
     * @return string
11068
     */
11069
    public static function tabs_to_spaces(string $str, int $tab_length = 4): string
11070
    {
11071 6
        if ($tab_length === 4) {
11072 3
            $spaces = '    ';
11073 3
        } elseif ($tab_length === 2) {
11074 1
            $spaces = '  ';
11075
        } else {
11076 2
            $spaces = \str_repeat(' ', $tab_length);
11077
        }
11078
11079 6
        return \str_replace("\t", $spaces, $str);
11080
    }
11081
11082
    /**
11083
     * Converts the first character of each word in the string to uppercase
11084
     * and all other chars to lowercase.
11085
     *
11086
     * @param string      $str                           <p>The input string.</p>
11087
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
11088
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
11089
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
11090
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
11091
     *
11092
     * @return string
11093
     *                <p>A string with all characters of $str being title-cased.</p>
11094
     */
11095
    public static function titlecase(
11096
        string $str,
11097
        string $encoding = 'UTF-8',
11098
        bool $clean_utf8 = false,
11099
        string $lang = null,
11100
        bool $try_to_keep_the_string_length = false
11101
    ): string {
11102 5
        if ($clean_utf8 === true) {
11103
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11104
            // if invalid characters are found in $haystack before $needle
11105
            $str = self::clean($str);
11106
        }
11107
11108 5
        if ($lang === null && $try_to_keep_the_string_length === false) {
11109 5
            if ($encoding === 'UTF-8') {
11110 3
                return \mb_convert_case($str, \MB_CASE_TITLE);
11111
            }
11112
11113 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11114
11115 2
            return \mb_convert_case($str, \MB_CASE_TITLE, $encoding);
11116
        }
11117
11118
        return self::str_titleize(
11119
            $str,
11120
            null,
11121
            $encoding,
11122
            false,
11123
            $lang,
11124
            $try_to_keep_the_string_length,
11125
            false
11126
        );
11127
    }
11128
11129
    /**
11130
     * alias for "UTF8::to_ascii()"
11131
     *
11132
     * @param string $str
11133
     * @param string $subst_chr
11134
     * @param bool   $strict
11135
     *
11136
     * @return string
11137
     *
11138
     * @see UTF8::to_ascii()
11139
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
11140
     */
11141
    public static function toAscii(
11142
        string $str,
11143
        string $subst_chr = '?',
11144
        bool $strict = false
11145
    ): string {
11146 7
        return self::to_ascii($str, $subst_chr, $strict);
11147
    }
11148
11149
    /**
11150
     * alias for "UTF8::to_iso8859()"
11151
     *
11152
     * @param string|string[] $str
11153
     *
11154
     * @return string|string[]
11155
     *
11156
     * @see UTF8::to_iso8859()
11157
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
11158
     */
11159
    public static function toIso8859($str)
11160
    {
11161 2
        return self::to_iso8859($str);
11162
    }
11163
11164
    /**
11165
     * alias for "UTF8::to_latin1()"
11166
     *
11167
     * @param string|string[] $str
11168
     *
11169
     * @return string|string[]
11170
     *
11171
     * @see UTF8::to_iso8859()
11172
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
11173
     */
11174
    public static function toLatin1($str)
11175
    {
11176 2
        return self::to_iso8859($str);
11177
    }
11178
11179
    /**
11180
     * alias for "UTF8::to_utf8()"
11181
     *
11182
     * @param string|string[] $str
11183
     *
11184
     * @return string|string[]
11185
     *
11186
     * @see UTF8::to_utf8()
11187
     * @deprecated <p>please use "UTF8::to_utf8()"</p>
11188
     */
11189
    public static function toUTF8($str)
11190
    {
11191 2
        return self::to_utf8($str);
11192
    }
11193
11194
    /**
11195
     * Convert a string into ASCII.
11196
     *
11197
     * @param string $str     <p>The input string.</p>
11198
     * @param string $unknown [optional] <p>Character use if character unknown. (default is ?)</p>
11199
     * @param bool   $strict  [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad
11200
     *                        performance</p>
11201
     *
11202
     * @return string
11203
     */
11204
    public static function to_ascii(
11205
        string $str,
11206
        string $unknown = '?',
11207
        bool $strict = false
11208
    ): string {
11209 37
        return ASCII::to_transliterate($str, $unknown, $strict);
11210
    }
11211
11212
    /**
11213
     * @param mixed $str
11214
     *
11215
     * @return bool
11216
     */
11217
    public static function to_boolean($str): bool
11218
    {
11219
        // init
11220 19
        $str = (string) $str;
11221
11222 19
        if ($str === '') {
11223 2
            return false;
11224
        }
11225
11226
        // Info: http://php.net/manual/en/filter.filters.validate.php
11227
        $map = [
11228 17
            'true'  => true,
11229
            '1'     => true,
11230
            'on'    => true,
11231
            'yes'   => true,
11232
            'false' => false,
11233
            '0'     => false,
11234
            'off'   => false,
11235
            'no'    => false,
11236
        ];
11237
11238 17
        if (isset($map[$str])) {
11239 11
            return $map[$str];
11240
        }
11241
11242 6
        $key = \strtolower($str);
11243 6
        if (isset($map[$key])) {
11244 2
            return $map[$key];
11245
        }
11246
11247 4
        if (\is_numeric($str)) {
11248 2
            return ((float) $str + 0) > 0;
11249
        }
11250
11251 2
        return (bool) \trim($str);
11252
    }
11253
11254
    /**
11255
     * Convert given string to safe filename (and keep string case).
11256
     *
11257
     * @param string $str
11258
     * @param bool   $use_transliterate No transliteration, conversion etc. is done by default - unsafe characters are
11259
     *                                  simply replaced with hyphen.
11260
     * @param string $fallback_char
11261
     *
11262
     * @return string
11263
     */
11264
    public static function to_filename(
11265
        string $str,
11266
        bool $use_transliterate = false,
11267
        string $fallback_char = '-'
11268
    ): string {
11269 1
        return ASCII::to_filename(
11270 1
            $str,
11271 1
            $use_transliterate,
11272 1
            $fallback_char
11273
        );
11274
    }
11275
11276
    /**
11277
     * Convert a string into "ISO-8859"-encoding (Latin-1).
11278
     *
11279
     * @param string|string[] $str
11280
     *
11281
     * @return string|string[]
11282
     */
11283
    public static function to_iso8859($str)
11284
    {
11285 8
        if (\is_array($str) === true) {
11286 2
            foreach ($str as $k => &$v) {
11287 2
                $v = self::to_iso8859($v);
11288
            }
11289
11290 2
            return $str;
11291
        }
11292
11293 8
        $str = (string) $str;
11294 8
        if ($str === '') {
11295 2
            return '';
11296
        }
11297
11298 8
        return self::utf8_decode($str);
11299
    }
11300
11301
    /**
11302
     * alias for "UTF8::to_iso8859()"
11303
     *
11304
     * @param string|string[] $str
11305
     *
11306
     * @return string|string[]
11307
     *
11308
     * @see UTF8::to_iso8859()
11309
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
11310
     */
11311
    public static function to_latin1($str)
11312
    {
11313 2
        return self::to_iso8859($str);
11314
    }
11315
11316
    /**
11317
     * This function leaves UTF-8 characters alone, while converting almost all non-UTF8 to UTF8.
11318
     *
11319
     * <ul>
11320
     * <li>It decode UTF-8 codepoints and Unicode escape sequences.</li>
11321
     * <li>It assumes that the encoding of the original string is either WINDOWS-1252 or ISO-8859.</li>
11322
     * <li>WARNING: It does not remove invalid UTF-8 characters, so you maybe need to use "UTF8::clean()" for this
11323
     * case.</li>
11324
     * </ul>
11325
     *
11326
     * @param string|string[] $str                        <p>Any string or array.</p>
11327
     * @param bool            $decode_html_entity_to_utf8 <p>Set to true, if you need to decode html-entities.</p>
11328
     *
11329
     * @return string|string[] the UTF-8 encoded string
11330
     */
11331
    public static function to_utf8($str, bool $decode_html_entity_to_utf8 = false)
11332
    {
11333 41
        if (\is_array($str) === true) {
11334 4
            foreach ($str as $k => &$v) {
11335 4
                $v = self::to_utf8($v, $decode_html_entity_to_utf8);
11336
            }
11337
11338 4
            return $str;
11339
        }
11340
11341 41
        $str = (string) $str;
11342 41
        if ($str === '') {
11343 6
            return $str;
11344
        }
11345
11346 41
        $max = \strlen($str);
11347 41
        $buf = '';
11348
11349 41
        for ($i = 0; $i < $max; ++$i) {
11350 41
            $c1 = $str[$i];
11351
11352 41
            if ($c1 >= "\xC0") { // should be converted to UTF8, if it's not UTF8 already
11353
11354 37
                if ($c1 <= "\xDF") { // looks like 2 bytes UTF8
11355
11356 34
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
11357
11358 34
                    if ($c2 >= "\x80" && $c2 <= "\xBF") { // yeah, almost sure it's UTF8 already
11359 20
                        $buf .= $c1 . $c2;
11360 20
                        ++$i;
11361
                    } else { // not valid UTF8 - convert it
11362 34
                        $buf .= self::to_utf8_convert_helper($c1);
11363
                    }
11364 34
                } elseif ($c1 >= "\xE0" && $c1 <= "\xEF") { // looks like 3 bytes UTF8
11365
11366 33
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
11367 33
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
11368
11369 33
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF") { // yeah, almost sure it's UTF8 already
11370 15
                        $buf .= $c1 . $c2 . $c3;
11371 15
                        $i += 2;
11372
                    } else { // not valid UTF8 - convert it
11373 33
                        $buf .= self::to_utf8_convert_helper($c1);
11374
                    }
11375 26
                } elseif ($c1 >= "\xF0" && $c1 <= "\xF7") { // looks like 4 bytes UTF8
11376
11377 26
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
11378 26
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
11379 26
                    $c4 = $i + 3 >= $max ? "\x00" : $str[$i + 3];
11380
11381 26
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF" && $c4 >= "\x80" && $c4 <= "\xBF") { // yeah, almost sure it's UTF8 already
11382 8
                        $buf .= $c1 . $c2 . $c3 . $c4;
11383 8
                        $i += 3;
11384
                    } else { // not valid UTF8 - convert it
11385 26
                        $buf .= self::to_utf8_convert_helper($c1);
11386
                    }
11387
                } else { // doesn't look like UTF8, but should be converted
11388
11389 37
                    $buf .= self::to_utf8_convert_helper($c1);
11390
                }
11391 38
            } elseif (($c1 & "\xC0") === "\x80") { // needs conversion
11392
11393 4
                $buf .= self::to_utf8_convert_helper($c1);
11394
            } else { // it doesn't need conversion
11395
11396 38
                $buf .= $c1;
11397
            }
11398
        }
11399
11400
        // decode unicode escape sequences + unicode surrogate pairs
11401 41
        $buf = \preg_replace_callback(
11402 41
            '/\\\\u([dD][89abAB][0-9a-fA-F]{2})\\\\u([dD][cdefCDEF][\da-fA-F]{2})|\\\\u([0-9a-fA-F]{4})/',
11403
            /**
11404
             * @param array $matches
11405
             *
11406
             * @return string
11407
             */
11408
            static function (array $matches): string {
11409 12
                if (isset($matches[3])) {
11410 12
                    $cp = (int) \hexdec($matches[3]);
11411
                } else {
11412
                    // http://unicode.org/faq/utf_bom.html#utf16-4
11413
                    $cp = ((int) \hexdec($matches[1]) << 10)
11414
                          + (int) \hexdec($matches[2])
11415
                          + 0x10000
11416
                          - (0xD800 << 10)
11417
                          - 0xDC00;
11418
                }
11419
11420
                // https://github.com/php/php-src/blob/php-7.3.2/ext/standard/html.c#L471
11421
                //
11422
                // php_utf32_utf8(unsigned char *buf, unsigned k)
11423
11424 12
                if ($cp < 0x80) {
11425 8
                    return (string) self::chr($cp);
11426
                }
11427
11428 9
                if ($cp < 0xA0) {
11429
                    /** @noinspection UnnecessaryCastingInspection */
11430
                    return (string) self::chr(0xC0 | $cp >> 6) . (string) self::chr(0x80 | $cp & 0x3F);
11431
                }
11432
11433 9
                return self::decimal_to_chr($cp);
11434 41
            },
11435 41
            $buf
11436
        );
11437
11438 41
        if ($buf === null) {
11439
            return '';
11440
        }
11441
11442
        // decode UTF-8 codepoints
11443 41
        if ($decode_html_entity_to_utf8 === true) {
11444 2
            $buf = self::html_entity_decode($buf);
11445
        }
11446
11447 41
        return $buf;
11448
    }
11449
11450
    /**
11451
     * Strip whitespace or other characters from the beginning and end of a UTF-8 string.
11452
     *
11453
     * INFO: This is slower then "trim()"
11454
     *
11455
     * We can only use the original-function, if we use <= 7-Bit in the string / chars
11456
     * but the check for ASCII (7-Bit) cost more time, then we can safe here.
11457
     *
11458
     * @param string      $str   <p>The string to be trimmed</p>
11459
     * @param string|null $chars [optional] <p>Optional characters to be stripped</p>
11460
     *
11461
     * @return string the trimmed string
11462
     */
11463
    public static function trim(string $str = '', string $chars = null): string
11464
    {
11465 56
        if ($str === '') {
11466 9
            return '';
11467
        }
11468
11469 49
        if (self::$SUPPORT['mbstring'] === true) {
11470
11471 49
            if ($chars) {
11472
                /** @noinspection PregQuoteUsageInspection */
11473 27
                $chars = \preg_quote($chars);
11474 27
                $pattern = "^[${chars}]+|[${chars}]+\$";
11475
            } else {
11476 22
                $pattern = '^[\\s]+|[\\s]+$';
11477
            }
11478
11479
            /** @noinspection PhpComposerExtensionStubsInspection */
11480 49
            return (string) \mb_ereg_replace($pattern, '', $str);
11481
        }
11482
11483 8
        if ($chars) {
11484
            $chars = \preg_quote($chars, '/');
11485
            $pattern = "^[${chars}]+|[${chars}]+\$";
11486
        } else {
11487 8
            $pattern = '^[\\s]+|[\\s]+$';
11488
        }
11489
11490 8
        return self::regex_replace($str, $pattern, '', '', '/');
11491
    }
11492
11493
    /**
11494
     * Makes string's first char uppercase.
11495
     *
11496
     * @param string      $str                           <p>The input string.</p>
11497
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
11498
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
11499
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
11500
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
11501
     *
11502
     * @return string the resulting string
11503
     */
11504
    public static function ucfirst(
11505
        string $str,
11506
        string $encoding = 'UTF-8',
11507
        bool $clean_utf8 = false,
11508
        string $lang = null,
11509
        bool $try_to_keep_the_string_length = false
11510
    ): string {
11511 69
        if ($str === '') {
11512 3
            return '';
11513
        }
11514
11515 68
        if ($clean_utf8 === true) {
11516
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11517
            // if invalid characters are found in $haystack before $needle
11518 1
            $str = self::clean($str);
11519
        }
11520
11521 68
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
11522
11523 68
        if ($encoding === 'UTF-8') {
11524 22
            $str_part_two = (string) \mb_substr($str, 1);
11525
11526 22
            if ($use_mb_functions === true) {
11527 22
                $str_part_one = \mb_strtoupper(
11528 22
                    (string) \mb_substr($str, 0, 1)
11529
                );
11530
            } else {
11531
                $str_part_one = self::strtoupper(
11532
                    (string) \mb_substr($str, 0, 1),
11533
                    $encoding,
11534
                    false,
11535
                    $lang,
11536 22
                    $try_to_keep_the_string_length
11537
                );
11538
            }
11539
        } else {
11540 47
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11541
11542 47
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
11543
11544 47
            if ($use_mb_functions === true) {
11545 47
                $str_part_one = \mb_strtoupper(
11546 47
                    (string) \mb_substr($str, 0, 1, $encoding),
11547 47
                    $encoding
11548
                );
11549
            } else {
11550
                $str_part_one = self::strtoupper(
11551
                    (string) self::substr($str, 0, 1, $encoding),
11552
                    $encoding,
11553
                    false,
11554
                    $lang,
11555
                    $try_to_keep_the_string_length
11556
                );
11557
            }
11558
        }
11559
11560 68
        return $str_part_one . $str_part_two;
11561
    }
11562
11563
    /**
11564
     * alias for "UTF8::ucfirst()"
11565
     *
11566
     * @param string $str
11567
     * @param string $encoding
11568
     * @param bool   $clean_utf8
11569
     *
11570
     * @return string
11571
     *
11572
     * @see UTF8::ucfirst()
11573
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
11574
     */
11575
    public static function ucword(
11576
        string $str,
11577
        string $encoding = 'UTF-8',
11578
        bool $clean_utf8 = false
11579
    ): string {
11580 1
        return self::ucfirst($str, $encoding, $clean_utf8);
11581
    }
11582
11583
    /**
11584
     * Uppercase for all words in the string.
11585
     *
11586
     * @param string   $str        <p>The input string.</p>
11587
     * @param string[] $exceptions [optional] <p>Exclusion for some words.</p>
11588
     * @param string   $char_list  [optional] <p>Additional chars that contains to words and do not start a new
11589
     *                             word.</p>
11590
     * @param string   $encoding   [optional] <p>Set the charset.</p>
11591
     * @param bool     $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11592
     *
11593
     * @return string
11594
     */
11595
    public static function ucwords(
11596
        string $str,
11597
        array $exceptions = [],
11598
        string $char_list = '',
11599
        string $encoding = 'UTF-8',
11600
        bool $clean_utf8 = false
11601
    ): string {
11602 8
        if (!$str) {
11603 2
            return '';
11604
        }
11605
11606
        // INFO: mb_convert_case($str, MB_CASE_TITLE);
11607
        // -> MB_CASE_TITLE didn't only uppercase the first letter, it also lowercase all other letters
11608
11609 7
        if ($clean_utf8 === true) {
11610
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11611
            // if invalid characters are found in $haystack before $needle
11612 1
            $str = self::clean($str);
11613
        }
11614
11615 7
        $use_php_default_functions = !(bool) ($char_list . \implode('', $exceptions));
11616
11617
        if (
11618 7
            $use_php_default_functions === true
11619
            &&
11620 7
            ASCII::is_ascii($str) === true
11621
        ) {
11622
            return \ucwords($str);
11623
        }
11624
11625 7
        $words = self::str_to_words($str, $char_list);
11626 7
        $use_exceptions = \count($exceptions) > 0;
11627
11628 7
        foreach ($words as &$word) {
11629 7
            if (!$word) {
11630 7
                continue;
11631
            }
11632
11633
            if (
11634 7
                $use_exceptions === false
11635
                ||
11636 7
                !\in_array($word, $exceptions, true)
11637
            ) {
11638 7
                $word = self::ucfirst($word, $encoding);
11639
            }
11640
        }
11641
11642 7
        return \implode('', $words);
11643
    }
11644
11645
    /**
11646
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
11647
     *
11648
     * e.g:
11649
     * 'test+test'                     => 'test test'
11650
     * 'D&#252;sseldorf'               => 'Düsseldorf'
11651
     * 'D%FCsseldorf'                  => 'Düsseldorf'
11652
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
11653
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
11654
     * 'Düsseldorf'                   => 'Düsseldorf'
11655
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
11656
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
11657
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
11658
     *
11659
     * @param string $str          <p>The input string.</p>
11660
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
11661
     *
11662
     * @return string
11663
     */
11664
    public static function urldecode(string $str, bool $multi_decode = true): string
11665
    {
11666 4
        if ($str === '') {
11667 3
            return '';
11668
        }
11669
11670
        if (
11671 4
            \strpos($str, '&') === false
11672
            &&
11673 4
            \strpos($str, '%') === false
11674
            &&
11675 4
            \strpos($str, '+') === false
11676
            &&
11677 4
            \strpos($str, '\u') === false
11678
        ) {
11679 3
            return self::fix_simple_utf8($str);
11680
        }
11681
11682 4
        $str = self::urldecode_unicode_helper($str);
11683
11684
        do {
11685 4
            $str_compare = $str;
11686
11687
            /**
11688
             * @psalm-suppress PossiblyInvalidArgument
11689
             */
11690 4
            $str = self::fix_simple_utf8(
11691 4
                \urldecode(
11692 4
                    self::html_entity_decode(
11693 4
                        self::to_utf8($str),
11694 4
                        \ENT_QUOTES | \ENT_HTML5
11695
                    )
11696
                )
11697
            );
11698 4
        } while ($multi_decode === true && $str_compare !== $str);
11699
11700 4
        return $str;
11701
    }
11702
11703
    /**
11704
     * Return a array with "urlencoded"-win1252 -> UTF-8
11705
     *
11706
     * @return string[]
11707
     *
11708
     * @deprecated <p>please use the "UTF8::urldecode()" function to decode a string</p>
11709
     */
11710
    public static function urldecode_fix_win1252_chars(): array
11711
    {
11712
        return [
11713 2
            '%20' => ' ',
11714
            '%21' => '!',
11715
            '%22' => '"',
11716
            '%23' => '#',
11717
            '%24' => '$',
11718
            '%25' => '%',
11719
            '%26' => '&',
11720
            '%27' => "'",
11721
            '%28' => '(',
11722
            '%29' => ')',
11723
            '%2A' => '*',
11724
            '%2B' => '+',
11725
            '%2C' => ',',
11726
            '%2D' => '-',
11727
            '%2E' => '.',
11728
            '%2F' => '/',
11729
            '%30' => '0',
11730
            '%31' => '1',
11731
            '%32' => '2',
11732
            '%33' => '3',
11733
            '%34' => '4',
11734
            '%35' => '5',
11735
            '%36' => '6',
11736
            '%37' => '7',
11737
            '%38' => '8',
11738
            '%39' => '9',
11739
            '%3A' => ':',
11740
            '%3B' => ';',
11741
            '%3C' => '<',
11742
            '%3D' => '=',
11743
            '%3E' => '>',
11744
            '%3F' => '?',
11745
            '%40' => '@',
11746
            '%41' => 'A',
11747
            '%42' => 'B',
11748
            '%43' => 'C',
11749
            '%44' => 'D',
11750
            '%45' => 'E',
11751
            '%46' => 'F',
11752
            '%47' => 'G',
11753
            '%48' => 'H',
11754
            '%49' => 'I',
11755
            '%4A' => 'J',
11756
            '%4B' => 'K',
11757
            '%4C' => 'L',
11758
            '%4D' => 'M',
11759
            '%4E' => 'N',
11760
            '%4F' => 'O',
11761
            '%50' => 'P',
11762
            '%51' => 'Q',
11763
            '%52' => 'R',
11764
            '%53' => 'S',
11765
            '%54' => 'T',
11766
            '%55' => 'U',
11767
            '%56' => 'V',
11768
            '%57' => 'W',
11769
            '%58' => 'X',
11770
            '%59' => 'Y',
11771
            '%5A' => 'Z',
11772
            '%5B' => '[',
11773
            '%5C' => '\\',
11774
            '%5D' => ']',
11775
            '%5E' => '^',
11776
            '%5F' => '_',
11777
            '%60' => '`',
11778
            '%61' => 'a',
11779
            '%62' => 'b',
11780
            '%63' => 'c',
11781
            '%64' => 'd',
11782
            '%65' => 'e',
11783
            '%66' => 'f',
11784
            '%67' => 'g',
11785
            '%68' => 'h',
11786
            '%69' => 'i',
11787
            '%6A' => 'j',
11788
            '%6B' => 'k',
11789
            '%6C' => 'l',
11790
            '%6D' => 'm',
11791
            '%6E' => 'n',
11792
            '%6F' => 'o',
11793
            '%70' => 'p',
11794
            '%71' => 'q',
11795
            '%72' => 'r',
11796
            '%73' => 's',
11797
            '%74' => 't',
11798
            '%75' => 'u',
11799
            '%76' => 'v',
11800
            '%77' => 'w',
11801
            '%78' => 'x',
11802
            '%79' => 'y',
11803
            '%7A' => 'z',
11804
            '%7B' => '{',
11805
            '%7C' => '|',
11806
            '%7D' => '}',
11807
            '%7E' => '~',
11808
            '%7F' => '',
11809
            '%80' => '`',
11810
            '%81' => '',
11811
            '%82' => '‚',
11812
            '%83' => 'ƒ',
11813
            '%84' => '„',
11814
            '%85' => '…',
11815
            '%86' => '†',
11816
            '%87' => '‡',
11817
            '%88' => 'ˆ',
11818
            '%89' => '‰',
11819
            '%8A' => 'Š',
11820
            '%8B' => '‹',
11821
            '%8C' => 'Œ',
11822
            '%8D' => '',
11823
            '%8E' => 'Ž',
11824
            '%8F' => '',
11825
            '%90' => '',
11826
            '%91' => '‘',
11827
            '%92' => '’',
11828
            '%93' => '“',
11829
            '%94' => '”',
11830
            '%95' => '•',
11831
            '%96' => '–',
11832
            '%97' => '—',
11833
            '%98' => '˜',
11834
            '%99' => '™',
11835
            '%9A' => 'š',
11836
            '%9B' => '›',
11837
            '%9C' => 'œ',
11838
            '%9D' => '',
11839
            '%9E' => 'ž',
11840
            '%9F' => 'Ÿ',
11841
            '%A0' => '',
11842
            '%A1' => '¡',
11843
            '%A2' => '¢',
11844
            '%A3' => '£',
11845
            '%A4' => '¤',
11846
            '%A5' => '¥',
11847
            '%A6' => '¦',
11848
            '%A7' => '§',
11849
            '%A8' => '¨',
11850
            '%A9' => '©',
11851
            '%AA' => 'ª',
11852
            '%AB' => '«',
11853
            '%AC' => '¬',
11854
            '%AD' => '',
11855
            '%AE' => '®',
11856
            '%AF' => '¯',
11857
            '%B0' => '°',
11858
            '%B1' => '±',
11859
            '%B2' => '²',
11860
            '%B3' => '³',
11861
            '%B4' => '´',
11862
            '%B5' => 'µ',
11863
            '%B6' => '¶',
11864
            '%B7' => '·',
11865
            '%B8' => '¸',
11866
            '%B9' => '¹',
11867
            '%BA' => 'º',
11868
            '%BB' => '»',
11869
            '%BC' => '¼',
11870
            '%BD' => '½',
11871
            '%BE' => '¾',
11872
            '%BF' => '¿',
11873
            '%C0' => 'À',
11874
            '%C1' => 'Á',
11875
            '%C2' => 'Â',
11876
            '%C3' => 'Ã',
11877
            '%C4' => 'Ä',
11878
            '%C5' => 'Å',
11879
            '%C6' => 'Æ',
11880
            '%C7' => 'Ç',
11881
            '%C8' => 'È',
11882
            '%C9' => 'É',
11883
            '%CA' => 'Ê',
11884
            '%CB' => 'Ë',
11885
            '%CC' => 'Ì',
11886
            '%CD' => 'Í',
11887
            '%CE' => 'Î',
11888
            '%CF' => 'Ï',
11889
            '%D0' => 'Ð',
11890
            '%D1' => 'Ñ',
11891
            '%D2' => 'Ò',
11892
            '%D3' => 'Ó',
11893
            '%D4' => 'Ô',
11894
            '%D5' => 'Õ',
11895
            '%D6' => 'Ö',
11896
            '%D7' => '×',
11897
            '%D8' => 'Ø',
11898
            '%D9' => 'Ù',
11899
            '%DA' => 'Ú',
11900
            '%DB' => 'Û',
11901
            '%DC' => 'Ü',
11902
            '%DD' => 'Ý',
11903
            '%DE' => 'Þ',
11904
            '%DF' => 'ß',
11905
            '%E0' => 'à',
11906
            '%E1' => 'á',
11907
            '%E2' => 'â',
11908
            '%E3' => 'ã',
11909
            '%E4' => 'ä',
11910
            '%E5' => 'å',
11911
            '%E6' => 'æ',
11912
            '%E7' => 'ç',
11913
            '%E8' => 'è',
11914
            '%E9' => 'é',
11915
            '%EA' => 'ê',
11916
            '%EB' => 'ë',
11917
            '%EC' => 'ì',
11918
            '%ED' => 'í',
11919
            '%EE' => 'î',
11920
            '%EF' => 'ï',
11921
            '%F0' => 'ð',
11922
            '%F1' => 'ñ',
11923
            '%F2' => 'ò',
11924
            '%F3' => 'ó',
11925
            '%F4' => 'ô',
11926
            '%F5' => 'õ',
11927
            '%F6' => 'ö',
11928
            '%F7' => '÷',
11929
            '%F8' => 'ø',
11930
            '%F9' => 'ù',
11931
            '%FA' => 'ú',
11932
            '%FB' => 'û',
11933
            '%FC' => 'ü',
11934
            '%FD' => 'ý',
11935
            '%FE' => 'þ',
11936
            '%FF' => 'ÿ',
11937
        ];
11938
    }
11939
11940
    /**
11941
     * Decodes a UTF-8 string to ISO-8859-1.
11942
     *
11943
     * @param string $str             <p>The input string.</p>
11944
     * @param bool   $keep_utf8_chars
11945
     *
11946
     * @return string
11947
     */
11948
    public static function utf8_decode(string $str, bool $keep_utf8_chars = false): string
11949
    {
11950 14
        if ($str === '') {
11951 6
            return '';
11952
        }
11953
11954
        // save for later comparision
11955 14
        $str_backup = $str;
11956 14
        $len = \strlen($str);
11957
11958 14
        if (self::$ORD === null) {
11959
            self::$ORD = self::getData('ord');
11960
        }
11961
11962 14
        if (self::$CHR === null) {
11963
            self::$CHR = self::getData('chr');
11964
        }
11965
11966 14
        $no_char_found = '?';
11967
        /** @noinspection ForeachInvariantsInspection */
11968 14
        for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
11969 14
            switch ($str[$i] & "\xF0") {
11970 14
                case "\xC0":
11971 13
                case "\xD0":
11972 13
                    $c = (self::$ORD[$str[$i] & "\x1F"] << 6) | self::$ORD[$str[++$i] & "\x3F"];
11973 13
                    $str[$j] = $c < 256 ? self::$CHR[$c] : $no_char_found;
11974
11975 13
                    break;
11976
11977
                /** @noinspection PhpMissingBreakStatementInspection */
11978 13
                case "\xF0":
11979
                    ++$i;
11980
11981
                // no break
11982
11983 13
                case "\xE0":
11984 11
                    $str[$j] = $no_char_found;
11985 11
                    $i += 2;
11986
11987 11
                    break;
11988
11989
                default:
11990 12
                    $str[$j] = $str[$i];
11991
            }
11992
        }
11993
11994
        /** @var false|string $return - needed for PhpStan (stubs error) */
11995 14
        $return = \substr($str, 0, $j);
11996 14
        if ($return === false) {
11997
            $return = '';
11998
        }
11999
12000
        if (
12001 14
            $keep_utf8_chars === true
12002
            &&
12003 14
            (int) self::strlen($return) >= (int) self::strlen($str_backup)
12004
        ) {
12005 2
            return $str_backup;
12006
        }
12007
12008 14
        return $return;
12009
    }
12010
12011
    /**
12012
     * Encodes an ISO-8859-1 string to UTF-8.
12013
     *
12014
     * @param string $str <p>The input string.</p>
12015
     *
12016
     * @return string
12017
     */
12018
    public static function utf8_encode(string $str): string
12019
    {
12020 14
        if ($str === '') {
12021 14
            return '';
12022
        }
12023
12024
        /** @var false|string $str - the polyfill maybe return false */
12025 14
        $str = \utf8_encode($str);
0 ignored issues
show
Bug introduced by
It seems like $str can also be of type false; however, parameter $data of utf8_encode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

12025
        $str = \utf8_encode(/** @scrutinizer ignore-type */ $str);
Loading history...
12026
12027
        /** @noinspection CallableParameterUseCaseInTypeContextInspection */
12028
        /** @psalm-suppress TypeDoesNotContainType */
12029 14
        if ($str === false) {
12030
            return '';
12031
        }
12032
12033 14
        return $str;
12034
    }
12035
12036
    /**
12037
     * fix -> utf8-win1252 chars
12038
     *
12039
     * @param string $str <p>The input string.</p>
12040
     *
12041
     * @return string
12042
     *
12043
     * @deprecated <p>please use "UTF8::fix_simple_utf8()"</p>
12044
     */
12045
    public static function utf8_fix_win1252_chars(string $str): string
12046
    {
12047 2
        return self::fix_simple_utf8($str);
12048
    }
12049
12050
    /**
12051
     * Returns an array with all utf8 whitespace characters.
12052
     *
12053
     * @see http://www.bogofilter.org/pipermail/bogofilter/2003-March/001889.html
12054
     *
12055
     * @return string[]
12056
     *                  An array with all known whitespace characters as values and the type of whitespace as keys
12057
     *                  as defined in above URL
12058
     */
12059
    public static function whitespace_table(): array
12060
    {
12061 2
        return self::$WHITESPACE_TABLE;
12062
    }
12063
12064
    /**
12065
     * Limit the number of words in a string.
12066
     *
12067
     * @param string $str        <p>The input string.</p>
12068
     * @param int    $limit      <p>The limit of words as integer.</p>
12069
     * @param string $str_add_on <p>Replacement for the striped string.</p>
12070
     *
12071
     * @return string
12072
     */
12073
    public static function words_limit(
12074
        string $str,
12075
        int $limit = 100,
12076
        string $str_add_on = '…'
12077
    ): string {
12078 2
        if ($str === '' || $limit < 1) {
12079 2
            return '';
12080
        }
12081
12082 2
        \preg_match('/^\\s*+(?:[^\\s]++\\s*+){1,' . $limit . '}/u', $str, $matches);
12083
12084
        if (
12085 2
            !isset($matches[0])
12086
            ||
12087 2
            \mb_strlen($str) === (int) \mb_strlen($matches[0])
12088
        ) {
12089 2
            return $str;
12090
        }
12091
12092 2
        return \rtrim($matches[0]) . $str_add_on;
12093
    }
12094
12095
    /**
12096
     * Wraps a string to a given number of characters
12097
     *
12098
     * @see http://php.net/manual/en/function.wordwrap.php
12099
     *
12100
     * @param string $str   <p>The input string.</p>
12101
     * @param int    $width [optional] <p>The column width.</p>
12102
     * @param string $break [optional] <p>The line is broken using the optional break parameter.</p>
12103
     * @param bool   $cut   [optional] <p>
12104
     *                      If the cut is set to true, the string is
12105
     *                      always wrapped at or before the specified width. So if you have
12106
     *                      a word that is larger than the given width, it is broken apart.
12107
     *                      </p>
12108
     *
12109
     * @return string
12110
     *                <p>The given string wrapped at the specified column.</p>
12111
     */
12112
    public static function wordwrap(
12113
        string $str,
12114
        int $width = 75,
12115
        string $break = "\n",
12116
        bool $cut = false
12117
    ): string {
12118 12
        if ($str === '' || $break === '') {
12119 4
            return '';
12120
        }
12121
12122 10
        $str_split = \explode($break, $str);
12123 10
        if ($str_split === false) {
12124
            return '';
12125
        }
12126
12127 10
        $chars = [];
12128 10
        $word_split = '';
12129 10
        foreach ($str_split as $i => $i_value) {
12130 10
            if ($i) {
12131 3
                $chars[] = $break;
12132 3
                $word_split .= '#';
12133
            }
12134
12135 10
            foreach (self::str_split($i_value) as $c) {
12136 10
                $chars[] = $c;
12137 10
                if ($c === ' ') {
12138 3
                    $word_split .= ' ';
12139
                } else {
12140 10
                    $word_split .= '?';
12141
                }
12142
            }
12143
        }
12144
12145 10
        $str_return = '';
12146 10
        $j = 0;
12147 10
        $b = -1;
12148 10
        $i = -1;
12149 10
        $word_split = \wordwrap($word_split, $width, '#', $cut);
12150
12151 10
        $max = \mb_strlen($word_split);
12152 10
        while (($b = \mb_strpos($word_split, '#', $b + 1)) !== false) {
12153 8
            for (++$i; $i < $b; ++$i) {
12154 8
                $str_return .= $chars[$j];
12155 8
                unset($chars[$j++]);
12156
12157
                // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
12158 8
                if ($i > $max) {
12159
                    break 2;
12160
                }
12161
            }
12162
12163
            if (
12164 8
                $break === $chars[$j]
12165
                ||
12166 8
                $chars[$j] === ' '
12167
            ) {
12168 5
                unset($chars[$j++]);
12169
            }
12170
12171 8
            $str_return .= $break;
12172
12173
            // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
12174 8
            if ($b > $max) {
12175
                break;
12176
            }
12177
        }
12178
12179 10
        return $str_return . \implode('', $chars);
12180
    }
12181
12182
    /**
12183
     * Line-Wrap the string after $limit, but split the string by "$delimiter" before ...
12184
     *    ... so that we wrap the per line.
12185
     *
12186
     * @param string      $str             <p>The input string.</p>
12187
     * @param int         $width           [optional] <p>The column width.</p>
12188
     * @param string      $break           [optional] <p>The line is broken using the optional break parameter.</p>
12189
     * @param bool        $cut             [optional] <p>
12190
     *                                     If the cut is set to true, the string is
12191
     *                                     always wrapped at or before the specified width. So if you have
12192
     *                                     a word that is larger than the given width, it is broken apart.
12193
     *                                     </p>
12194
     * @param bool        $add_final_break [optional] <p>
12195
     *                                     If this flag is true, then the method will add a $break at the end
12196
     *                                     of the result string.
12197
     *                                     </p>
12198
     * @param string|null $delimiter       [optional] <p>
12199
     *                                     You can change the default behavior, where we split the string by newline.
12200
     *                                     </p>
12201
     *
12202
     * @return string
12203
     */
12204
    public static function wordwrap_per_line(
12205
        string $str,
12206
        int $width = 75,
12207
        string $break = "\n",
12208
        bool $cut = false,
12209
        bool $add_final_break = true,
12210
        string $delimiter = null
12211
    ): string {
12212 1
        if ($delimiter === null) {
12213 1
            $strings = \preg_split('/\\r\\n|\\r|\\n/', $str);
12214
        } else {
12215 1
            $strings = \explode($delimiter, $str);
12216
        }
12217
12218 1
        $string_helper_array = [];
12219 1
        if ($strings !== false) {
12220 1
            foreach ($strings as $value) {
12221 1
                $string_helper_array[] = self::wordwrap($value, $width, $break, $cut);
12222
            }
12223
        }
12224
12225 1
        if ($add_final_break) {
12226 1
            $final_break = $break;
12227
        } else {
12228 1
            $final_break = '';
12229
        }
12230
12231 1
        return \implode($delimiter ?? "\n", $string_helper_array) . $final_break;
12232
    }
12233
12234
    /**
12235
     * Returns an array of Unicode White Space characters.
12236
     *
12237
     * @return string[] an array with numeric code point as key and White Space Character as value
12238
     */
12239
    public static function ws(): array
12240
    {
12241 2
        return self::$WHITESPACE;
12242
    }
12243
12244
    /**
12245
     * @param string $str
12246
     * @param string $encoding
12247
     *
12248
     * @return string
12249
     */
12250
    private static function html_entity_decode_helper(string $str, string $encoding)
12251
    {
12252
        return (string) \preg_replace_callback(
12253
            "/&#\d{2,6};/",
12254
            /**
12255
             * @param string[] $matches
12256
             *
12257
             * @return string
12258
             */
12259
            static function (array $matches) use ($encoding): string {
12260
                $return_tmp = \mb_convert_encoding($matches[0], $encoding, 'HTML-ENTITIES');
12261
                if ($return_tmp !== '"' && $return_tmp !== "'") {
12262
                    return $return_tmp;
12263
                }
12264
12265
                return $matches[0];
12266
            },
12267
            $str
12268
        );
12269
    }
12270
12271
    /**
12272
     * Checks whether the passed string contains only byte sequences that are valid UTF-8 characters.
12273
     *
12274
     * @see http://hsivonen.iki.fi/php-utf8/
12275
     *
12276
     * @param string $str    <p>The string to be checked.</p>
12277
     * @param bool   $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
12278
     *
12279
     * @return bool
12280
     */
12281
    private static function is_utf8_string(string $str, bool $strict = false)
12282
    {
12283 108
        if ($str === '') {
12284 14
            return true;
12285
        }
12286
12287 102
        if ($strict === true) {
12288 2
            $is_binary = self::is_binary($str, true);
12289
12290 2
            if ($is_binary && self::is_utf16($str, false) !== false) {
12291 2
                return false;
12292
            }
12293
12294
            if ($is_binary && self::is_utf32($str, false) !== false) {
12295
                return false;
12296
            }
12297
        }
12298
12299 102
        if (self::pcre_utf8_support() !== true) {
12300
            // If even just the first character can be matched, when the /u
12301
            // modifier is used, then it's valid UTF-8. If the UTF-8 is somehow
12302
            // invalid, nothing at all will match, even if the string contains
12303
            // some valid sequences
12304
            return \preg_match('/^./us', $str, $ar) === 1;
12305
        }
12306
12307 102
        $mState = 0; // cached expected number of octets after the current octet
12308
        // until the beginning of the next UTF8 character sequence
12309 102
        $mUcs4 = 0; // cached Unicode character
12310 102
        $mBytes = 1; // cached expected number of octets in the current sequence
12311
12312 102
        if (self::$ORD === null) {
12313
            self::$ORD = self::getData('ord');
12314
        }
12315
12316 102
        $len = \strlen($str);
12317
        /** @noinspection ForeachInvariantsInspection */
12318 102
        for ($i = 0; $i < $len; ++$i) {
12319 102
            $in = self::$ORD[$str[$i]];
12320
12321 102
            if ($mState === 0) {
12322
                // When mState is zero we expect either a US-ASCII character or a
12323
                // multi-octet sequence.
12324 102
                if ((0x80 & $in) === 0) {
12325
                    // US-ASCII, pass straight through.
12326 97
                    $mBytes = 1;
12327 83
                } elseif ((0xE0 & $in) === 0xC0) {
12328
                    // First octet of 2 octet sequence.
12329 73
                    $mUcs4 = $in;
12330 73
                    $mUcs4 = ($mUcs4 & 0x1F) << 6;
12331 73
                    $mState = 1;
12332 73
                    $mBytes = 2;
12333 58
                } elseif ((0xF0 & $in) === 0xE0) {
12334
                    // First octet of 3 octet sequence.
12335 42
                    $mUcs4 = $in;
12336 42
                    $mUcs4 = ($mUcs4 & 0x0F) << 12;
12337 42
                    $mState = 2;
12338 42
                    $mBytes = 3;
12339 29
                } elseif ((0xF8 & $in) === 0xF0) {
12340
                    // First octet of 4 octet sequence.
12341 18
                    $mUcs4 = $in;
12342 18
                    $mUcs4 = ($mUcs4 & 0x07) << 18;
12343 18
                    $mState = 3;
12344 18
                    $mBytes = 4;
12345 13
                } elseif ((0xFC & $in) === 0xF8) {
12346
                    /* First octet of 5 octet sequence.
12347
                     *
12348
                     * This is illegal because the encoded codepoint must be either
12349
                     * (a) not the shortest form or
12350
                     * (b) outside the Unicode range of 0-0x10FFFF.
12351
                     * Rather than trying to resynchronize, we will carry on until the end
12352
                     * of the sequence and let the later error handling code catch it.
12353
                     */
12354 5
                    $mUcs4 = $in;
12355 5
                    $mUcs4 = ($mUcs4 & 0x03) << 24;
12356 5
                    $mState = 4;
12357 5
                    $mBytes = 5;
12358 10
                } elseif ((0xFE & $in) === 0xFC) {
12359
                    // First octet of 6 octet sequence, see comments for 5 octet sequence.
12360 5
                    $mUcs4 = $in;
12361 5
                    $mUcs4 = ($mUcs4 & 1) << 30;
12362 5
                    $mState = 5;
12363 5
                    $mBytes = 6;
12364
                } else {
12365
                    // Current octet is neither in the US-ASCII range nor a legal first
12366
                    // octet of a multi-octet sequence.
12367 102
                    return false;
12368
                }
12369 83
            } elseif ((0xC0 & $in) === 0x80) {
12370
12371
                // When mState is non-zero, we expect a continuation of the multi-octet
12372
                // sequence
12373
12374
                // Legal continuation.
12375 75
                $shift = ($mState - 1) * 6;
12376 75
                $tmp = $in;
12377 75
                $tmp = ($tmp & 0x0000003F) << $shift;
12378 75
                $mUcs4 |= $tmp;
12379
                // Prefix: End of the multi-octet sequence. mUcs4 now contains the final
12380
                // Unicode code point to be output.
12381 75
                if (--$mState === 0) {
12382
                    // Check for illegal sequences and code points.
12383
                    //
12384
                    // From Unicode 3.1, non-shortest form is illegal
12385
                    if (
12386 75
                        ($mBytes === 2 && $mUcs4 < 0x0080)
12387
                        ||
12388 75
                        ($mBytes === 3 && $mUcs4 < 0x0800)
12389
                        ||
12390 75
                        ($mBytes === 4 && $mUcs4 < 0x10000)
12391
                        ||
12392 75
                        ($mBytes > 4)
12393
                        ||
12394
                        // From Unicode 3.2, surrogate characters are illegal.
12395 75
                        (($mUcs4 & 0xFFFFF800) === 0xD800)
12396
                        ||
12397
                        // Code points outside the Unicode range are illegal.
12398 75
                        ($mUcs4 > 0x10FFFF)
12399
                    ) {
12400 8
                        return false;
12401
                    }
12402
                    // initialize UTF8 cache
12403 75
                    $mState = 0;
12404 75
                    $mUcs4 = 0;
12405 75
                    $mBytes = 1;
12406
                }
12407
            } else {
12408
                // ((0xC0 & (*in) != 0x80) && (mState != 0))
12409
                // Incomplete multi-octet sequence.
12410 35
                return false;
12411
            }
12412
        }
12413
12414 67
        return true;
12415
    }
12416
12417
    /**
12418
     * @param string $str
12419
     * @param bool   $use_lowercase      <p>Use uppercase by default, otherwise use lowercase.</p>
12420
     * @param bool   $use_full_case_fold <p>Convert not only common cases.</p>
12421
     *
12422
     * @return string
12423
     */
12424
    private static function fixStrCaseHelper(
12425
        string $str,
12426
        $use_lowercase = false,
12427
        $use_full_case_fold = false
12428
    ) {
12429 33
        $upper = self::$COMMON_CASE_FOLD['upper'];
12430 33
        $lower = self::$COMMON_CASE_FOLD['lower'];
12431
12432 33
        if ($use_lowercase === true) {
12433 2
            $str = \str_replace(
12434 2
                $upper,
12435 2
                $lower,
12436 2
                $str
12437
            );
12438
        } else {
12439 31
            $str = \str_replace(
12440 31
                $lower,
12441 31
                $upper,
12442 31
                $str
12443
            );
12444
        }
12445
12446 33
        if ($use_full_case_fold) {
12447 31
            static $FULL_CASE_FOLD = null;
12448 31
            if ($FULL_CASE_FOLD === null) {
12449 1
                $FULL_CASE_FOLD = self::getData('caseFolding_full');
12450
            }
12451
12452 31
            if ($use_lowercase === true) {
12453 2
                $str = \str_replace($FULL_CASE_FOLD[0], $FULL_CASE_FOLD[1], $str);
12454
            } else {
12455 29
                $str = \str_replace($FULL_CASE_FOLD[1], $FULL_CASE_FOLD[0], $str);
12456
            }
12457
        }
12458
12459 33
        return $str;
12460
    }
12461
12462
    /**
12463
     * get data from "/data/*.php"
12464
     *
12465
     * @param string $file
12466
     *
12467
     * @return array
12468
     */
12469
    private static function getData(string $file)
12470
    {
12471
        /** @noinspection PhpIncludeInspection */
12472
        /** @noinspection UsingInclusionReturnValueInspection */
12473
        /** @psalm-suppress UnresolvableInclude */
12474 6
        return include __DIR__ . '/data/' . $file . '.php';
12475
    }
12476
12477
    /**
12478
     * @return true|null
12479
     */
12480
    private static function initEmojiData()
12481
    {
12482 12
        if (self::$EMOJI_KEYS_CACHE === null) {
12483 1
            if (self::$EMOJI === null) {
12484 1
                self::$EMOJI = self::getData('emoji');
12485
            }
12486
12487 1
            \uksort(
12488 1
                self::$EMOJI,
12489
                static function (string $a, string $b): int {
12490 1
                    return \strlen($b) <=> \strlen($a);
12491 1
                }
12492
            );
12493
12494 1
            self::$EMOJI_KEYS_CACHE = \array_keys(self::$EMOJI);
12495 1
            self::$EMOJI_VALUES_CACHE = \array_values(self::$EMOJI);
12496
12497 1
            foreach (self::$EMOJI_KEYS_CACHE as $key) {
12498 1
                $tmp_key = \crc32($key);
12499 1
                self::$EMOJI_KEYS_REVERSIBLE_CACHE[] = '_-_PORTABLE_UTF8_-_' . $tmp_key . '_-_' . \strrev((string) $tmp_key) . '_-_8FTU_ELBATROP_-_';
12500
            }
12501
12502 1
            return true;
12503
        }
12504
12505 12
        return null;
12506
    }
12507
12508
    /**
12509
     * Checks whether mbstring "overloaded" is active on the server.
12510
     *
12511
     * @return bool
12512
     */
12513
    private static function mbstring_overloaded()
12514
    {
12515
        /**
12516
         * INI directive 'mbstring.func_overload' is deprecated since PHP 7.2
12517
         */
12518
12519
        /** @noinspection PhpComposerExtensionStubsInspection */
12520
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
12521
        return \defined('MB_OVERLOAD_STRING')
12522
               &&
12523
               ((int) @\ini_get('mbstring.func_overload') & \MB_OVERLOAD_STRING);
12524
    }
12525
12526
    /**
12527
     * @param array    $strings
12528
     * @param bool     $remove_empty_values
12529
     * @param int|null $remove_short_values
12530
     *
12531
     * @return array
12532
     */
12533
    private static function reduce_string_array(
12534
        array $strings,
12535
        bool $remove_empty_values,
12536
        int $remove_short_values = null
12537
    ) {
12538
        // init
12539 2
        $return = [];
12540
12541 2
        foreach ($strings as &$str) {
12542
            if (
12543 2
                $remove_short_values !== null
12544
                &&
12545 2
                \mb_strlen($str) <= $remove_short_values
12546
            ) {
12547 2
                continue;
12548
            }
12549
12550
            if (
12551 2
                $remove_empty_values === true
12552
                &&
12553 2
                \trim($str) === ''
12554
            ) {
12555 2
                continue;
12556
            }
12557
12558 2
            $return[] = $str;
12559
        }
12560
12561 2
        return $return;
12562
    }
12563
12564
    /**
12565
     * rxClass
12566
     *
12567
     * @param string $s
12568
     * @param string $class
12569
     *
12570
     * @return string
12571
     */
12572
    private static function rxClass(string $s, string $class = '')
12573
    {
12574 33
        static $RX_CLASS_CACHE = [];
12575
12576 33
        $cache_key = $s . $class;
12577
12578 33
        if (isset($RX_CLASS_CACHE[$cache_key])) {
12579 21
            return $RX_CLASS_CACHE[$cache_key];
12580
        }
12581
12582 16
        $class_array = [$class];
12583
12584
        /** @noinspection SuspiciousLoopInspection */
12585
        /** @noinspection AlterInForeachInspection */
12586 16
        foreach (self::str_split($s) as &$s) {
0 ignored issues
show
Bug introduced by
The expression self::str_split($s) cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
12587 15
            if ($s === '-') {
12588
                $class_array[0] = '-' . $class_array[0];
12589 15
            } elseif (!isset($s[2])) {
12590 15
                $class_array[0] .= \preg_quote($s, '/');
12591 1
            } elseif (self::strlen($s) === 1) {
12592 1
                $class_array[0] .= $s;
12593
            } else {
12594 15
                $class_array[] = $s;
12595
            }
12596
        }
12597
12598 16
        if ($class_array[0]) {
12599 16
            $class_array[0] = '[' . $class_array[0] . ']';
12600
        }
12601
12602 16
        if (\count($class_array) === 1) {
12603 16
            $return = $class_array[0];
12604
        } else {
12605
            $return = '(?:' . \implode('|', $class_array) . ')';
12606
        }
12607
12608 16
        $RX_CLASS_CACHE[$cache_key] = $return;
12609
12610 16
        return $return;
12611
    }
12612
12613
    /**
12614
     * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
12615
     *
12616
     * @param string $names
12617
     * @param string $delimiter
12618
     * @param string $encoding
12619
     *
12620
     * @return string
12621
     */
12622
    private static function str_capitalize_name_helper(string $names, string $delimiter, string $encoding = 'UTF-8')
12623
    {
12624
        // init
12625 1
        $name_helper_array = \explode($delimiter, $names);
12626 1
        if ($name_helper_array === false) {
12627
            return '';
12628
        }
12629
12630
        $special_cases = [
12631 1
            'names' => [
12632
                'ab',
12633
                'af',
12634
                'al',
12635
                'and',
12636
                'ap',
12637
                'bint',
12638
                'binte',
12639
                'da',
12640
                'de',
12641
                'del',
12642
                'den',
12643
                'der',
12644
                'di',
12645
                'dit',
12646
                'ibn',
12647
                'la',
12648
                'mac',
12649
                'nic',
12650
                'of',
12651
                'ter',
12652
                'the',
12653
                'und',
12654
                'van',
12655
                'von',
12656
                'y',
12657
                'zu',
12658
            ],
12659
            'prefixes' => [
12660
                'al-',
12661
                "d'",
12662
                'ff',
12663
                "l'",
12664
                'mac',
12665
                'mc',
12666
                'nic',
12667
            ],
12668
        ];
12669
12670 1
        foreach ($name_helper_array as &$name) {
12671 1
            if (\in_array($name, $special_cases['names'], true)) {
12672 1
                continue;
12673
            }
12674
12675 1
            $continue = false;
12676
12677 1
            if ($delimiter === '-') {
12678
                /** @noinspection AlterInForeachInspection */
12679 1
                foreach ((array) $special_cases['names'] as &$beginning) {
12680 1
                    if (self::strpos($name, $beginning, 0, $encoding) === 0) {
12681 1
                        $continue = true;
12682
                    }
12683
                }
12684
            }
12685
12686
            /** @noinspection AlterInForeachInspection */
12687 1
            foreach ((array) $special_cases['prefixes'] as &$beginning) {
12688 1
                if (self::strpos($name, $beginning, 0, $encoding) === 0) {
12689 1
                    $continue = true;
12690
                }
12691
            }
12692
12693 1
            if ($continue === true) {
12694 1
                continue;
12695
            }
12696
12697 1
            $name = self::ucfirst($name);
12698
        }
12699
12700 1
        return \implode($delimiter, $name_helper_array);
12701
    }
12702
12703
    /**
12704
     * Generic case-sensitive transformation for collation matching.
12705
     *
12706
     * @param string $str <p>The input string</p>
12707
     *
12708
     * @return string|null
12709
     */
12710
    private static function strtonatfold(string $str)
12711
    {
12712
        /** @noinspection PhpUndefinedClassInspection */
12713 6
        return \preg_replace(
12714 6
            '/\p{Mn}+/u',
12715 6
            '',
12716 6
            \Normalizer::normalize($str, \Normalizer::NFD)
12717
        );
12718
    }
12719
12720
    /**
12721
     * @param int|string $input
12722
     *
12723
     * @return string
12724
     */
12725
    private static function to_utf8_convert_helper($input)
12726
    {
12727
        // init
12728 31
        $buf = '';
12729
12730 31
        if (self::$ORD === null) {
12731 1
            self::$ORD = self::getData('ord');
12732
        }
12733
12734 31
        if (self::$CHR === null) {
12735 1
            self::$CHR = self::getData('chr');
12736
        }
12737
12738 31
        if (self::$WIN1252_TO_UTF8 === null) {
12739 1
            self::$WIN1252_TO_UTF8 = self::getData('win1252_to_utf8');
12740
        }
12741
12742 31
        $ordC1 = self::$ORD[$input];
12743 31
        if (isset(self::$WIN1252_TO_UTF8[$ordC1])) { // found in Windows-1252 special cases
12744 31
            $buf .= self::$WIN1252_TO_UTF8[$ordC1];
12745
        } else {
12746
            /** @noinspection OffsetOperationsInspection */
12747 1
            $cc1 = self::$CHR[$ordC1 / 64] | "\xC0";
12748 1
            $cc2 = ((string) $input & "\x3F") | "\x80";
0 ignored issues
show
Bug introduced by
Are you sure you want to use the bitwise | or did you mean ||?
Loading history...
Bug introduced by
Are you sure you want to use the bitwise & or did you mean &&?
Loading history...
12749 1
            $buf .= $cc1 . $cc2;
12750
        }
12751
12752 31
        return $buf;
12753
    }
12754
12755
    /**
12756
     * @param string $str
12757
     *
12758
     * @return string
12759
     */
12760
    private static function urldecode_unicode_helper(string $str)
12761
    {
12762 9
        $pattern = '/%u([0-9a-fA-F]{3,4})/';
12763 9
        if (\preg_match($pattern, $str)) {
12764 7
            $str = (string) \preg_replace($pattern, '&#x\\1;', $str);
12765
        }
12766
12767 9
        return $str;
12768
    }
12769
}
12770