Passed
Push — master ( 0d46c6...14681c )
by Lars
03:47
created

UTF8::is_alpha()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 8
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 10
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[]}
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
        if (!\is_array($arg)) {
0 ignored issues
show
introduced by
The condition is_array($arg) is always true.
Loading history...
871 4
            return [];
872
        }
873
874 12
        if ($arg === []) {
875 7
            return [];
876
        }
877
878 11
        $arg = \array_map(
879
            [
880 11
                self::class,
881
                'ord',
882
            ],
883 11
            $arg
884
        );
885
886 11
        if ($u_style === true) {
887 2
            $arg = \array_map(
888
                [
889 2
                    self::class,
890
                    'int_to_hex',
891
                ],
892 2
                $arg
893
            );
894
        }
895
896 11
        return $arg;
897
    }
898
899
    /**
900
     * Trims the string and replaces consecutive whitespace characters with a
901
     * single space. This includes tabs and newline characters, as well as
902
     * multibyte whitespace such as the thin space and ideographic space.
903
     *
904
     * @param string $str <p>The input string.</p>
905
     *
906
     * @return string string with a trimmed $str and condensed whitespace
907
     */
908 13
    public static function collapse_whitespace(string $str): string
909
    {
910 13
        if (self::$SUPPORT['mbstring'] === true) {
911
            /** @noinspection PhpComposerExtensionStubsInspection */
912 13
            return \trim((string) \mb_ereg_replace('[[:space:]]+', ' ', $str));
913
        }
914
915
        return \trim(self::regex_replace($str, '[[:space:]]+', ' '));
916
    }
917
918
    /**
919
     * Returns count of characters used in a string.
920
     *
921
     * @param string $str                     <p>The input string.</p>
922
     * @param bool   $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
923
     * @param bool   $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
924
     *
925
     * @return int[] an associative array of Character as keys and
926
     *               their count as values
927
     */
928 19
    public static function count_chars(
929
        string $str,
930
        bool $clean_utf8 = false,
931
        bool $try_to_use_mb_functions = true
932
    ): array {
933 19
        return \array_count_values(
934 19
            self::str_split(
935 19
                $str,
936 19
                1,
937 19
                $clean_utf8,
938 19
                $try_to_use_mb_functions
939
            )
940
        );
941
    }
942
943
    /**
944
     * Remove css media-queries.
945
     *
946
     * @param string $str
947
     *
948
     * @return string
949
     */
950 1
    public static function css_stripe_media_queries(string $str): string
951
    {
952 1
        return (string) \preg_replace(
953 1
            '#@media\\s+(?:only\\s)?(?:[\\s{(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#isumU',
954 1
            '',
955 1
            $str
956
        );
957
    }
958
959
    /**
960
     * Checks whether ctype is available on the server.
961
     *
962
     * @return bool
963
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
964
     */
965
    public static function ctype_loaded(): bool
966
    {
967
        return \extension_loaded('ctype');
968
    }
969
970
    /**
971
     * Converts an int value into a UTF-8 character.
972
     *
973
     * @param mixed $int
974
     *
975
     * @return string
976
     */
977 19
    public static function decimal_to_chr($int): string
978
    {
979 19
        return self::html_entity_decode('&#' . $int . ';', \ENT_QUOTES | \ENT_HTML5);
980
    }
981
982
    /**
983
     * Decodes a MIME header field
984
     *
985
     * @param string $str
986
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
987
     *
988
     * @return false|string
989
     *                      A decoded MIME field on success,
990
     *                      or false if an error occurs during the decoding
991
     */
992
    public static function decode_mimeheader($str, string $encoding = 'UTF-8')
993
    {
994
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
995
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
996
        }
997
998
        if (self::$SUPPORT['iconv'] === true) {
999
            return \iconv_mime_decode($str, \ICONV_MIME_DECODE_CONTINUE_ON_ERROR, $encoding);
1000
        }
1001
1002
        if ($encoding !== 'UTF-8') {
1003
            $str = self::encode($encoding, $str);
1004
        }
1005
1006
        return \mb_decode_mimeheader($str);
1007
    }
1008
1009
    /**
1010
     * Decodes a string which was encoded by "UTF8::emoji_encode()".
1011
     *
1012
     * @param string $str                            <p>The input string.</p>
1013
     * @param bool   $use_reversible_string_mappings [optional] <p>
1014
     *                                               When <b>TRUE</b>, we se a reversible string mapping
1015
     *                                               between "emoji_encode" and "emoji_decode".</p>
1016
     *
1017
     * @return string
1018
     */
1019 9
    public static function emoji_decode(
1020
        string $str,
1021
        bool $use_reversible_string_mappings = false
1022
    ): string {
1023 9
        self::initEmojiData();
1024
1025 9
        if ($use_reversible_string_mappings === true) {
1026 9
            return (string) \str_replace(
1027 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1028 9
                (array) self::$EMOJI_VALUES_CACHE,
1029 9
                $str
1030
            );
1031
        }
1032
1033 1
        return (string) \str_replace(
1034 1
            (array) self::$EMOJI_KEYS_CACHE,
1035 1
            (array) self::$EMOJI_VALUES_CACHE,
1036 1
            $str
1037
        );
1038
    }
1039
1040
    /**
1041
     * Encode a string with emoji chars into a non-emoji string.
1042
     *
1043
     * @param string $str                            <p>The input string</p>
1044
     * @param bool   $use_reversible_string_mappings [optional] <p>
1045
     *                                               when <b>TRUE</b>, we se a reversible string mapping
1046
     *                                               between "emoji_encode" and "emoji_decode"</p>
1047
     *
1048
     * @return string
1049
     */
1050 12
    public static function emoji_encode(
1051
        string $str,
1052
        bool $use_reversible_string_mappings = false
1053
    ): string {
1054 12
        self::initEmojiData();
1055
1056 12
        if ($use_reversible_string_mappings === true) {
1057 9
            return (string) \str_replace(
1058 9
                (array) self::$EMOJI_VALUES_CACHE,
1059 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1060 9
                $str
1061
            );
1062
        }
1063
1064 4
        return (string) \str_replace(
1065 4
            (array) self::$EMOJI_VALUES_CACHE,
1066 4
            (array) self::$EMOJI_KEYS_CACHE,
1067 4
            $str
1068
        );
1069
    }
1070
1071
    /**
1072
     * Encode a string with a new charset-encoding.
1073
     *
1074
     * INFO:  This function will also try to fix broken / double encoding,
1075
     *        so you can call this function also on a UTF-8 string and you don't mess up the string.
1076
     *
1077
     * @param string $to_encoding                   <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.</p>
1078
     * @param string $str                           <p>The input string</p>
1079
     * @param bool   $auto_detect_the_from_encoding [optional] <p>Force the new encoding (we try to fix broken / double
1080
     *                                              encoding for UTF-8)<br> otherwise we auto-detect the current
1081
     *                                              string-encoding</p>
1082
     * @param string $from_encoding                 [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1083
     *                                              A empty string will trigger the autodetect anyway.</p>
1084
     *
1085
     * @return string
1086
     *
1087
     * @psalm-suppress InvalidReturnStatement
1088
     */
1089 28
    public static function encode(
1090
        string $to_encoding,
1091
        string $str,
1092
        bool $auto_detect_the_from_encoding = true,
1093
        string $from_encoding = ''
1094
    ): string {
1095 28
        if ($str === '' || $to_encoding === '') {
1096 13
            return $str;
1097
        }
1098
1099 28
        if ($to_encoding !== 'UTF-8' && $to_encoding !== 'CP850') {
1100 7
            $to_encoding = self::normalize_encoding($to_encoding, 'UTF-8');
1101
        }
1102
1103 28
        if ($from_encoding && $from_encoding !== 'UTF-8' && $from_encoding !== 'CP850') {
1104 2
            $from_encoding = self::normalize_encoding($from_encoding, null);
1105
        }
1106
1107
        if (
1108 28
            $to_encoding
1109
            &&
1110 28
            $from_encoding
1111
            &&
1112 28
            $from_encoding === $to_encoding
1113
        ) {
1114
            return $str;
1115
        }
1116
1117 28
        if ($to_encoding === 'JSON') {
1118 1
            $return = self::json_encode($str);
1119 1
            if ($return === false) {
1120
                throw new \InvalidArgumentException('The input string [' . $str . '] can not be used for json_encode().');
1121
            }
1122
1123 1
            return $return;
1124
        }
1125 28
        if ($from_encoding === 'JSON') {
1126 1
            $str = self::json_decode($str);
1127 1
            $from_encoding = '';
1128
        }
1129
1130 28
        if ($to_encoding === 'BASE64') {
1131 2
            return \base64_encode($str);
1132
        }
1133 28
        if ($from_encoding === 'BASE64') {
1134 2
            $str = \base64_decode($str, true);
1135 2
            $from_encoding = '';
1136
        }
1137
1138 28
        if ($to_encoding === 'HTML-ENTITIES') {
1139 2
            return self::html_encode($str, true, 'UTF-8');
1140
        }
1141 28
        if ($from_encoding === 'HTML-ENTITIES') {
1142 2
            $str = self::html_entity_decode($str, \ENT_COMPAT, 'UTF-8');
1143 2
            $from_encoding = '';
1144
        }
1145
1146 28
        $from_encoding_auto_detected = false;
1147
        if (
1148 28
            $auto_detect_the_from_encoding === true
1149
            ||
1150 28
            !$from_encoding
1151
        ) {
1152 28
            $from_encoding_auto_detected = self::str_detect_encoding($str);
1153
        }
1154
1155
        // DEBUG
1156
        //var_dump($to_encoding, $from_encoding, $from_encoding_auto_detected, $str, "\n\n");
1157
1158 28
        if ($from_encoding_auto_detected !== false) {
1159
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1160 24
            $from_encoding = $from_encoding_auto_detected;
1161 7
        } elseif ($auto_detect_the_from_encoding === true) {
1162
            // fallback for the "autodetect"-mode
1163 7
            return self::to_utf8($str);
1164
        }
1165
1166
        if (
1167 24
            !$from_encoding
1168
            ||
1169 24
            $from_encoding === $to_encoding
1170
        ) {
1171 15
            return $str;
1172
        }
1173
1174
        if (
1175 19
            $to_encoding === 'UTF-8'
1176
            &&
1177
            (
1178 17
                $from_encoding === 'WINDOWS-1252'
1179
                ||
1180 19
                $from_encoding === 'ISO-8859-1'
1181
            )
1182
        ) {
1183 13
            return self::to_utf8($str);
1184
        }
1185
1186
        if (
1187 12
            $to_encoding === 'ISO-8859-1'
1188
            &&
1189
            (
1190 6
                $from_encoding === 'WINDOWS-1252'
1191
                ||
1192 12
                $from_encoding === 'UTF-8'
1193
            )
1194
        ) {
1195 6
            return self::to_iso8859($str);
1196
        }
1197
1198
        if (
1199 10
            $to_encoding !== 'UTF-8'
1200
            &&
1201 10
            $to_encoding !== 'ISO-8859-1'
1202
            &&
1203 10
            $to_encoding !== 'WINDOWS-1252'
1204
            &&
1205 10
            self::$SUPPORT['mbstring'] === false
1206
        ) {
1207
            \trigger_error('UTF8::encode() without mbstring cannot handle "' . $to_encoding . '" encoding', \E_USER_WARNING);
1208
        }
1209
1210 10
        if (self::$SUPPORT['mbstring'] === true) {
1211
            // warning: do not use the symfony polyfill here
1212 10
            $str_encoded = \mb_convert_encoding(
1213 10
                $str,
1214 10
                $to_encoding,
1215 10
                $from_encoding
1216
            );
1217
1218 10
            if ($str_encoded) {
1219 10
                return $str_encoded;
1220
            }
1221
        }
1222
1223
        $return = \iconv($from_encoding, $to_encoding, $str);
1224
        if ($return !== false) {
1225
            return $return;
1226
        }
1227
1228
        return $str;
1229
    }
1230
1231
    /**
1232
     * @param string $str
1233
     * @param string $from_charset      [optional] <p>Set the input charset.</p>
1234
     * @param string $to_charset        [optional] <p>Set the output charset.</p>
1235
     * @param string $transfer_encoding [optional] <p>Set the transfer encoding.</p>
1236
     * @param string $linefeed          [optional] <p>Set the used linefeed.</p>
1237
     * @param int    $indent            [optional] <p>Set the max length indent.</p>
1238
     *
1239
     * @return false|string
1240
     *                      <p>An encoded MIME field on success,
1241
     *                      or false if an error occurs during the encoding.</p>
1242
     */
1243
    public static function encode_mimeheader(
1244
        $str,
1245
        $from_charset = 'UTF-8',
1246
        $to_charset = 'UTF-8',
1247
        $transfer_encoding = 'Q',
1248
        $linefeed = '\\r\\n',
1249
        $indent = 76
1250
    ) {
1251
        if ($from_charset !== 'UTF-8' && $from_charset !== 'CP850') {
1252
            $from_charset = self::normalize_encoding($from_charset, 'UTF-8');
1253
        }
1254
1255
        if ($to_charset !== 'UTF-8' && $to_charset !== 'CP850') {
1256
            $to_charset = self::normalize_encoding($to_charset, 'UTF-8');
1257
        }
1258
1259
        return \iconv_mime_encode(
1260
            '',
1261
            $str,
1262
            [
1263
                'scheme'           => $transfer_encoding,
1264
                'line-length'      => $indent,
1265
                'input-charset'    => $from_charset,
1266
                'output-charset'   => $to_charset,
1267
                'line-break-chars' => $linefeed,
1268
            ]
1269
        );
1270
    }
1271
1272
    /**
1273
     * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
1274
     *
1275
     * @param string   $str                       <p>The input string.</p>
1276
     * @param string   $search                    <p>The searched string.</p>
1277
     * @param int|null $length                    [optional] <p>Default: null === text->length / 2</p>
1278
     * @param string   $replacer_for_skipped_text [optional] <p>Default: …</p>
1279
     * @param string   $encoding                  [optional] <p>Set the charset for e.g. "mb_" function</p>
1280
     *
1281
     * @return string
1282
     */
1283 1
    public static function extract_text(
1284
        string $str,
1285
        string $search = '',
1286
        int $length = null,
1287
        string $replacer_for_skipped_text = '…',
1288
        string $encoding = 'UTF-8'
1289
    ): string {
1290 1
        if ($str === '') {
1291 1
            return '';
1292
        }
1293
1294 1
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1295
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1296
        }
1297
1298 1
        $trim_chars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1299
1300 1
        if ($length === null) {
1301 1
            $length = (int) \round((int) self::strlen($str, $encoding) / 2, 0);
1302
        }
1303
1304 1
        if ($search === '') {
1305 1
            if ($encoding === 'UTF-8') {
1306 1
                if ($length > 0) {
1307 1
                    $string_length = (int) \mb_strlen($str);
1308 1
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1309
                } else {
1310 1
                    $end = 0;
1311
                }
1312
1313 1
                $pos = (int) \min(
1314 1
                    \mb_strpos($str, ' ', $end),
1315 1
                    \mb_strpos($str, '.', $end)
1316
                );
1317
            } else {
1318
                if ($length > 0) {
1319
                    $string_length = (int) self::strlen($str, $encoding);
1320
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1321
                } else {
1322
                    $end = 0;
1323
                }
1324
1325
                $pos = (int) \min(
1326
                    self::strpos($str, ' ', $end, $encoding),
1327
                    self::strpos($str, '.', $end, $encoding)
1328
                );
1329
            }
1330
1331 1
            if ($pos) {
1332 1
                if ($encoding === 'UTF-8') {
1333 1
                    $str_sub = \mb_substr($str, 0, $pos);
1334
                } else {
1335
                    $str_sub = self::substr($str, 0, $pos, $encoding);
1336
                }
1337
1338 1
                if ($str_sub === false) {
1339
                    return '';
1340
                }
1341
1342 1
                return \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1343
            }
1344
1345
            return $str;
1346
        }
1347
1348 1
        if ($encoding === 'UTF-8') {
1349 1
            $word_position = (int) \mb_stripos($str, $search);
1350 1
            $half_side = (int) ($word_position - $length / 2 + (int) \mb_strlen($search) / 2);
1351
        } else {
1352
            $word_position = (int) self::stripos($str, $search, 0, $encoding);
1353
            $half_side = (int) ($word_position - $length / 2 + (int) self::strlen($search, $encoding) / 2);
1354
        }
1355
1356 1
        $pos_start = 0;
1357 1
        if ($half_side > 0) {
1358 1
            if ($encoding === 'UTF-8') {
1359 1
                $half_text = \mb_substr($str, 0, $half_side);
1360
            } else {
1361
                $half_text = self::substr($str, 0, $half_side, $encoding);
1362
            }
1363 1
            if ($half_text !== false) {
1364 1
                if ($encoding === 'UTF-8') {
1365 1
                    $pos_start = (int) \max(
1366 1
                        \mb_strrpos($half_text, ' '),
1367 1
                        \mb_strrpos($half_text, '.')
1368
                    );
1369
                } else {
1370
                    $pos_start = (int) \max(
1371
                        self::strrpos($half_text, ' ', 0, $encoding),
1372
                        self::strrpos($half_text, '.', 0, $encoding)
1373
                    );
1374
                }
1375
            }
1376
        }
1377
1378 1
        if ($word_position && $half_side > 0) {
1379 1
            $offset = $pos_start + $length - 1;
1380 1
            $real_length = (int) self::strlen($str, $encoding);
1381
1382 1
            if ($offset > $real_length) {
1383
                $offset = $real_length;
1384
            }
1385
1386 1
            if ($encoding === 'UTF-8') {
1387 1
                $pos_end = (int) \min(
1388 1
                    \mb_strpos($str, ' ', $offset),
1389 1
                    \mb_strpos($str, '.', $offset)
1390 1
                ) - $pos_start;
1391
            } else {
1392
                $pos_end = (int) \min(
1393
                    self::strpos($str, ' ', $offset, $encoding),
1394
                    self::strpos($str, '.', $offset, $encoding)
1395
                ) - $pos_start;
1396
            }
1397
1398 1
            if (!$pos_end || $pos_end <= 0) {
1399 1
                if ($encoding === 'UTF-8') {
1400 1
                    $str_sub = \mb_substr($str, $pos_start, (int) \mb_strlen($str));
1401
                } else {
1402
                    $str_sub = self::substr($str, $pos_start, (int) self::strlen($str, $encoding), $encoding);
1403
                }
1404 1
                if ($str_sub !== false) {
1405 1
                    $extract = $replacer_for_skipped_text . \ltrim($str_sub, $trim_chars);
1406
                } else {
1407 1
                    $extract = '';
1408
                }
1409
            } else {
1410 1
                if ($encoding === 'UTF-8') {
1411 1
                    $str_sub = \mb_substr($str, $pos_start, $pos_end);
1412
                } else {
1413
                    $str_sub = self::substr($str, $pos_start, $pos_end, $encoding);
1414
                }
1415 1
                if ($str_sub !== false) {
1416 1
                    $extract = $replacer_for_skipped_text . \trim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1417
                } else {
1418 1
                    $extract = '';
1419
                }
1420
            }
1421
        } else {
1422 1
            $offset = $length - 1;
1423 1
            $true_length = (int) self::strlen($str, $encoding);
1424
1425 1
            if ($offset > $true_length) {
1426
                $offset = $true_length;
1427
            }
1428
1429 1
            if ($encoding === 'UTF-8') {
1430 1
                $pos_end = (int) \min(
1431 1
                    \mb_strpos($str, ' ', $offset),
1432 1
                    \mb_strpos($str, '.', $offset)
1433
                );
1434
            } else {
1435
                $pos_end = (int) \min(
1436
                    self::strpos($str, ' ', $offset, $encoding),
1437
                    self::strpos($str, '.', $offset, $encoding)
1438
                );
1439
            }
1440
1441 1
            if ($pos_end) {
1442 1
                if ($encoding === 'UTF-8') {
1443 1
                    $str_sub = \mb_substr($str, 0, $pos_end);
1444
                } else {
1445
                    $str_sub = self::substr($str, 0, $pos_end, $encoding);
1446
                }
1447 1
                if ($str_sub !== false) {
1448 1
                    $extract = \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1449
                } else {
1450 1
                    $extract = '';
1451
                }
1452
            } else {
1453 1
                $extract = $str;
1454
            }
1455
        }
1456
1457 1
        return $extract;
1458
    }
1459
1460
    /**
1461
     * Reads entire file into a string.
1462
     *
1463
     * WARNING: Do not use UTF-8 Option ($convert_to_utf8) for binary files (e.g.: images) !!!
1464
     *
1465
     * @see http://php.net/manual/en/function.file-get-contents.php
1466
     *
1467
     * @param string        $filename         <p>
1468
     *                                        Name of the file to read.
1469
     *                                        </p>
1470
     * @param bool          $use_include_path [optional] <p>
1471
     *                                        Prior to PHP 5, this parameter is called
1472
     *                                        use_include_path and is a bool.
1473
     *                                        As of PHP 5 the FILE_USE_INCLUDE_PATH can be used
1474
     *                                        to trigger include path
1475
     *                                        search.
1476
     *                                        </p>
1477
     * @param resource|null $context          [optional] <p>
1478
     *                                        A valid context resource created with
1479
     *                                        stream_context_create. If you don't need to use a
1480
     *                                        custom context, you can skip this parameter by &null;.
1481
     *                                        </p>
1482
     * @param int|null      $offset           [optional] <p>
1483
     *                                        The offset where the reading starts.
1484
     *                                        </p>
1485
     * @param int|null      $max_length       [optional] <p>
1486
     *                                        Maximum length of data read. The default is to read until end
1487
     *                                        of file is reached.
1488
     *                                        </p>
1489
     * @param int           $timeout          <p>The time in seconds for the timeout.</p>
1490
     * @param bool          $convert_to_utf8  <strong>WARNING!!!</strong> <p>Maybe you can't use this option for
1491
     *                                        some files, because they used non default utf-8 chars. Binary files
1492
     *                                        like images or pdf will not be converted.</p>
1493
     * @param string        $from_encoding    [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1494
     *                                        A empty string will trigger the autodetect anyway.</p>
1495
     *
1496
     * @return false|string
1497
     *                      <p>The function returns the read data as string or <b>false</b> on failure.</p>
1498
     */
1499 12
    public static function file_get_contents(
1500
        string $filename,
1501
        bool $use_include_path = false,
1502
        $context = null,
1503
        int $offset = null,
1504
        int $max_length = null,
1505
        int $timeout = 10,
1506
        bool $convert_to_utf8 = true,
1507
        string $from_encoding = ''
1508
    ) {
1509
        // init
1510 12
        $filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
1511
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1512 12
        if ($filename === false) {
1513
            return false;
1514
        }
1515
1516 12
        if ($timeout && $context === null) {
1517 9
            $context = \stream_context_create(
1518
                [
1519
                    'http' => [
1520 9
                        'timeout' => $timeout,
1521
                    ],
1522
                ]
1523
            );
1524
        }
1525
1526 12
        if ($offset === null) {
1527 12
            $offset = 0;
1528
        }
1529
1530 12
        if (\is_int($max_length) === true) {
1531 2
            $data = \file_get_contents($filename, $use_include_path, $context, $offset, $max_length);
1532
        } else {
1533 12
            $data = \file_get_contents($filename, $use_include_path, $context, $offset);
1534
        }
1535
1536
        // return false on error
1537 12
        if ($data === false) {
1538
            return false;
1539
        }
1540
1541 12
        if ($convert_to_utf8 === true) {
1542
            if (
1543 12
                self::is_binary($data, true) !== true
1544
                ||
1545 9
                self::is_utf16($data, false) !== false
1546
                ||
1547 12
                self::is_utf32($data, false) !== false
1548
            ) {
1549 9
                $data = self::encode('UTF-8', $data, false, $from_encoding);
1550 9
                $data = self::cleanup($data);
1551
            }
1552
        }
1553
1554 12
        return $data;
1555
    }
1556
1557
    /**
1558
     * Checks if a file starts with BOM (Byte Order Mark) character.
1559
     *
1560
     * @param string $file_path <p>Path to a valid file.</p>
1561
     *
1562
     * @throws \RuntimeException if file_get_contents() returned false
1563
     *
1564
     * @return bool
1565
     *              <p><strong>true</strong> if the file has BOM at the start, <strong>false</strong> otherwise</p>
1566
     */
1567 2
    public static function file_has_bom(string $file_path): bool
1568
    {
1569 2
        $file_content = \file_get_contents($file_path);
1570 2
        if ($file_content === false) {
1571
            throw new \RuntimeException('file_get_contents() returned false for:' . $file_path);
1572
        }
1573
1574 2
        return self::string_has_bom($file_content);
1575
    }
1576
1577
    /**
1578
     * Normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1579
     *
1580
     * @param mixed  $var
1581
     * @param int    $normalization_form
1582
     * @param string $leading_combining
1583
     *
1584
     * @return mixed
1585
     */
1586 62
    public static function filter(
1587
        $var,
1588
        int $normalization_form = \Normalizer::NFC,
1589
        string $leading_combining = '◌'
1590
    ) {
1591 62
        switch (\gettype($var)) {
1592 62
            case 'array':
1593
                /** @noinspection ForeachSourceInspection */
1594 6
                foreach ($var as $k => &$v) {
1595 6
                    $v = self::filter($v, $normalization_form, $leading_combining);
1596
                }
1597 6
                unset($v);
1598
1599 6
                break;
1600 62
            case 'object':
1601
                /** @noinspection ForeachSourceInspection */
1602 4
                foreach ($var as $k => &$v) {
1603 4
                    $v = self::filter($v, $normalization_form, $leading_combining);
1604
                }
1605 4
                unset($v);
1606
1607 4
                break;
1608 62
            case 'string':
1609
1610 62
                if (\strpos($var, "\r") !== false) {
1611
                    // Workaround https://bugs.php.net/65732
1612 3
                    $var = self::normalize_line_ending($var);
1613
                }
1614
1615 62
                if (ASCII::is_ascii($var) === false) {
1616 32
                    if (\Normalizer::isNormalized($var, $normalization_form)) {
1617 27
                        $n = '-';
1618
                    } else {
1619 12
                        $n = \Normalizer::normalize($var, $normalization_form);
1620
1621 12
                        if (isset($n[0])) {
1622 7
                            $var = $n;
1623
                        } else {
1624 8
                            $var = self::encode('UTF-8', $var, true);
1625
                        }
1626
                    }
1627
1628
                    if (
1629 32
                        $var[0] >= "\x80"
1630
                        &&
1631 32
                        isset($n[0], $leading_combining[0])
1632
                        &&
1633 32
                        \preg_match('/^\\p{Mn}/u', $var)
1634
                    ) {
1635
                        // Prevent leading combining chars
1636
                        // for NFC-safe concatenations.
1637 3
                        $var = $leading_combining . $var;
1638
                    }
1639
                }
1640
1641 62
                break;
1642
        }
1643
1644 62
        return $var;
1645
    }
1646
1647
    /**
1648
     * "filter_input()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1649
     *
1650
     * Gets a specific external variable by name and optionally filters it
1651
     *
1652
     * @see http://php.net/manual/en/function.filter-input.php
1653
     *
1654
     * @param int    $type          <p>
1655
     *                              One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1656
     *                              <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1657
     *                              <b>INPUT_ENV</b>.
1658
     *                              </p>
1659
     * @param string $variable_name <p>
1660
     *                              Name of a variable to get.
1661
     *                              </p>
1662
     * @param int    $filter        [optional] <p>
1663
     *                              The ID of the filter to apply. The
1664
     *                              manual page lists the available filters.
1665
     *                              </p>
1666
     * @param mixed  $options       [optional] <p>
1667
     *                              Associative array of options or bitwise disjunction of flags. If filter
1668
     *                              accepts options, flags can be provided in "flags" field of array.
1669
     *                              </p>
1670
     *
1671
     * @return mixed Value of the requested variable on success, <b>FALSE</b> if the filter fails, or <b>NULL</b> if the
1672
     *               <i>variable_name</i> variable is not set. If the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it
1673
     *               returns <b>FALSE</b> if the variable is not set and <b>NULL</b> if the filter fails.
1674
     */
1675
    public static function filter_input(
1676
        int $type,
1677
        string $variable_name,
1678
        int $filter = \FILTER_DEFAULT,
1679
        $options = null
1680
    ) {
1681
        if (\func_num_args() < 4) {
1682
            $var = \filter_input($type, $variable_name, $filter);
1683
        } else {
1684
            $var = \filter_input($type, $variable_name, $filter, $options);
1685
        }
1686
1687
        return self::filter($var);
1688
    }
1689
1690
    /**
1691
     * "filter_input_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1692
     *
1693
     * Gets external variables and optionally filters them
1694
     *
1695
     * @see http://php.net/manual/en/function.filter-input-array.php
1696
     *
1697
     * @param int   $type       <p>
1698
     *                          One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1699
     *                          <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1700
     *                          <b>INPUT_ENV</b>.
1701
     *                          </p>
1702
     * @param mixed $definition [optional] <p>
1703
     *                          An array defining the arguments. A valid key is a string
1704
     *                          containing a variable name and a valid value is either a filter type, or an array
1705
     *                          optionally specifying the filter, flags and options. If the value is an
1706
     *                          array, valid keys are filter which specifies the
1707
     *                          filter type,
1708
     *                          flags which specifies any flags that apply to the
1709
     *                          filter, and options which specifies any options that
1710
     *                          apply to the filter. See the example below for a better understanding.
1711
     *                          </p>
1712
     *                          <p>
1713
     *                          This parameter can be also an integer holding a filter constant. Then all values in the
1714
     *                          input array are filtered by this filter.
1715
     *                          </p>
1716
     * @param bool  $add_empty  [optional] <p>
1717
     *                          Add missing keys as <b>NULL</b> to the return value.
1718
     *                          </p>
1719
     *
1720
     * @return mixed An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1721
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1722
     *               set. Or if the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it returns <b>FALSE</b> if the variable
1723
     *               is not set and <b>NULL</b> if the filter fails.
1724
     */
1725
    public static function filter_input_array(
1726
        int $type,
1727
        $definition = null,
1728
        bool $add_empty = true
1729
    ) {
1730
        if (\func_num_args() < 2) {
1731
            $a = \filter_input_array($type);
1732
        } else {
1733
            $a = \filter_input_array($type, $definition, $add_empty);
1734
        }
1735
1736
        return self::filter($a);
1737
    }
1738
1739
    /**
1740
     * "filter_var()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1741
     *
1742
     * Filters a variable with a specified filter
1743
     *
1744
     * @see http://php.net/manual/en/function.filter-var.php
1745
     *
1746
     * @param mixed $variable <p>
1747
     *                        Value to filter.
1748
     *                        </p>
1749
     * @param int   $filter   [optional] <p>
1750
     *                        The ID of the filter to apply. The
1751
     *                        manual page lists the available filters.
1752
     *                        </p>
1753
     * @param mixed $options  [optional] <p>
1754
     *                        Associative array of options or bitwise disjunction of flags. If filter
1755
     *                        accepts options, flags can be provided in "flags" field of array. For
1756
     *                        the "callback" filter, callable type should be passed. The
1757
     *                        callback must accept one argument, the value to be filtered, and return
1758
     *                        the value after filtering/sanitizing it.
1759
     *                        </p>
1760
     *                        <p>
1761
     *                        <code>
1762
     *                        // for filters that accept options, use this format
1763
     *                        $options = array(
1764
     *                        'options' => array(
1765
     *                        'default' => 3, // value to return if the filter fails
1766
     *                        // other options here
1767
     *                        'min_range' => 0
1768
     *                        ),
1769
     *                        'flags' => FILTER_FLAG_ALLOW_OCTAL,
1770
     *                        );
1771
     *                        $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
1772
     *                        // for filter that only accept flags, you can pass them directly
1773
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
1774
     *                        // for filter that only accept flags, you can also pass as an array
1775
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN,
1776
     *                        array('flags' => FILTER_NULL_ON_FAILURE));
1777
     *                        // callback validate filter
1778
     *                        function foo($value)
1779
     *                        {
1780
     *                        // Expected format: Surname, GivenNames
1781
     *                        if (strpos($value, ", ") === false) return false;
1782
     *                        list($surname, $givennames) = explode(", ", $value, 2);
1783
     *                        $empty = (empty($surname) || empty($givennames));
1784
     *                        $notstrings = (!is_string($surname) || !is_string($givennames));
1785
     *                        if ($empty || $notstrings) {
1786
     *                        return false;
1787
     *                        } else {
1788
     *                        return $value;
1789
     *                        }
1790
     *                        }
1791
     *                        $var = filter_var('Doe, Jane Sue', FILTER_CALLBACK, array('options' => 'foo'));
1792
     *                        </code>
1793
     *                        </p>
1794
     *
1795
     * @return mixed the filtered data, or <b>FALSE</b> if the filter fails
1796
     */
1797 2
    public static function filter_var(
1798
        $variable,
1799
        int $filter = \FILTER_DEFAULT,
1800
        $options = null
1801
    ) {
1802 2
        if (\func_num_args() < 3) {
1803 2
            $variable = \filter_var($variable, $filter);
1804
        } else {
1805 2
            $variable = \filter_var($variable, $filter, $options);
1806
        }
1807
1808 2
        return self::filter($variable);
1809
    }
1810
1811
    /**
1812
     * "filter_var_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1813
     *
1814
     * Gets multiple variables and optionally filters them
1815
     *
1816
     * @see http://php.net/manual/en/function.filter-var-array.php
1817
     *
1818
     * @param array $data       <p>
1819
     *                          An array with string keys containing the data to filter.
1820
     *                          </p>
1821
     * @param mixed $definition [optional] <p>
1822
     *                          An array defining the arguments. A valid key is a string
1823
     *                          containing a variable name and a valid value is either a
1824
     *                          filter type, or an
1825
     *                          array optionally specifying the filter, flags and options.
1826
     *                          If the value is an array, valid keys are filter
1827
     *                          which specifies the filter type,
1828
     *                          flags which specifies any flags that apply to the
1829
     *                          filter, and options which specifies any options that
1830
     *                          apply to the filter. See the example below for a better understanding.
1831
     *                          </p>
1832
     *                          <p>
1833
     *                          This parameter can be also an integer holding a filter constant. Then all values in the
1834
     *                          input array are filtered by this filter.
1835
     *                          </p>
1836
     * @param bool  $add_empty  [optional] <p>
1837
     *                          Add missing keys as <b>NULL</b> to the return value.
1838
     *                          </p>
1839
     *
1840
     * @return mixed an array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1841
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1842
     *               set
1843
     */
1844 2
    public static function filter_var_array(
1845
        array $data,
1846
        $definition = null,
1847
        bool $add_empty = true
1848
    ) {
1849 2
        if (\func_num_args() < 2) {
1850 2
            $a = \filter_var_array($data);
1851
        } else {
1852 2
            $a = \filter_var_array($data, $definition, $add_empty);
1853
        }
1854
1855 2
        return self::filter($a);
1856
    }
1857
1858
    /**
1859
     * Checks whether finfo is available on the server.
1860
     *
1861
     * @return bool
1862
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
1863
     */
1864
    public static function finfo_loaded(): bool
1865
    {
1866
        return \class_exists('finfo');
1867
    }
1868
1869
    /**
1870
     * Returns the first $n characters of the string.
1871
     *
1872
     * @param string $str      <p>The input string.</p>
1873
     * @param int    $n        <p>Number of characters to retrieve from the start.</p>
1874
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
1875
     *
1876
     * @return string
1877
     */
1878 13
    public static function first_char(
1879
        string $str,
1880
        int $n = 1,
1881
        string $encoding = 'UTF-8'
1882
    ): string {
1883 13
        if ($str === '' || $n <= 0) {
1884 5
            return '';
1885
        }
1886
1887 8
        if ($encoding === 'UTF-8') {
1888 4
            return (string) \mb_substr($str, 0, $n);
1889
        }
1890
1891 4
        return (string) self::substr($str, 0, $n, $encoding);
1892
    }
1893
1894
    /**
1895
     * Check if the number of Unicode characters isn't greater than the specified integer.
1896
     *
1897
     * @param string $str      the original string to be checked
1898
     * @param int    $box_size the size in number of chars to be checked against string
1899
     *
1900
     * @return bool true if string is less than or equal to $box_size, false otherwise
1901
     */
1902 2
    public static function fits_inside(string $str, int $box_size): bool
1903
    {
1904 2
        return (int) self::strlen($str) <= $box_size;
1905
    }
1906
1907
    /**
1908
     * Try to fix simple broken UTF-8 strings.
1909
     *
1910
     * INFO: Take a look at "UTF8::fix_utf8()" if you need a more advanced fix for broken UTF-8 strings.
1911
     *
1912
     * If you received an UTF-8 string that was converted from Windows-1252 as it was ISO-8859-1
1913
     * (ignoring Windows-1252 chars from 80 to 9F) use this function to fix it.
1914
     * See: http://en.wikipedia.org/wiki/Windows-1252
1915
     *
1916
     * @param string $str <p>The input string</p>
1917
     *
1918
     * @return string
1919
     */
1920 46
    public static function fix_simple_utf8(string $str): string
1921
    {
1922 46
        if ($str === '') {
1923 4
            return '';
1924
        }
1925
1926 46
        static $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = null;
1927 46
        static $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = null;
1928
1929 46
        if ($BROKEN_UTF8_TO_UTF8_KEYS_CACHE === null) {
1930 1
            if (self::$BROKEN_UTF8_FIX === null) {
1931 1
                self::$BROKEN_UTF8_FIX = self::getData('utf8_fix');
1932
            }
1933
1934 1
            $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = \array_keys(self::$BROKEN_UTF8_FIX);
1935 1
            $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = \array_values(self::$BROKEN_UTF8_FIX);
1936
        }
1937
1938 46
        return \str_replace($BROKEN_UTF8_TO_UTF8_KEYS_CACHE, $BROKEN_UTF8_TO_UTF8_VALUES_CACHE, $str);
1939
    }
1940
1941
    /**
1942
     * Fix a double (or multiple) encoded UTF8 string.
1943
     *
1944
     * @param string|string[] $str you can use a string or an array of strings
1945
     *
1946
     * @return string|string[]
1947
     *                         Will return the fixed input-"array" or
1948
     *                         the fixed input-"string"
1949
     *
1950
     * @psalm-suppress InvalidReturnType
1951
     */
1952 2
    public static function fix_utf8($str)
1953
    {
1954 2
        if (\is_array($str) === true) {
1955 2
            foreach ($str as $k => &$v) {
1956 2
                $v = self::fix_utf8($v);
1957
            }
1958 2
            unset($v);
1959
1960
            /**
1961
             * @psalm-suppress InvalidReturnStatement
1962
             */
1963 2
            return $str;
1964
        }
1965
1966 2
        $str = (string) $str;
1967 2
        $last = '';
1968 2
        while ($last !== $str) {
1969 2
            $last = $str;
1970
            /**
1971
             * @psalm-suppress PossiblyInvalidArgument
1972
             */
1973 2
            $str = self::to_utf8(
1974 2
                self::utf8_decode($str, true)
1975
            );
1976
        }
1977
1978
        /**
1979
         * @psalm-suppress InvalidReturnStatement
1980
         */
1981 2
        return $str;
1982
    }
1983
1984
    /**
1985
     * Get character of a specific character.
1986
     *
1987
     * @param string $char
1988
     *
1989
     * @return string 'RTL' or 'LTR'
1990
     */
1991 2
    public static function getCharDirection(string $char): string
1992
    {
1993 2
        if (self::$SUPPORT['intlChar'] === true) {
1994
            /** @noinspection PhpComposerExtensionStubsInspection */
1995 2
            $tmp_return = \IntlChar::charDirection($char);
1996
1997
            // from "IntlChar"-Class
1998
            $char_direction = [
1999 2
                'RTL' => [1, 13, 14, 15, 21],
2000
                'LTR' => [0, 11, 12, 20],
2001
            ];
2002
2003 2
            if (\in_array($tmp_return, $char_direction['LTR'], true)) {
2004
                return 'LTR';
2005
            }
2006
2007 2
            if (\in_array($tmp_return, $char_direction['RTL'], true)) {
2008 2
                return 'RTL';
2009
            }
2010
        }
2011
2012 2
        $c = static::chr_to_decimal($char);
2013
2014 2
        if (!($c >= 0x5be && $c <= 0x10b7f)) {
2015 2
            return 'LTR';
2016
        }
2017
2018 2
        if ($c <= 0x85e) {
2019 2
            if ($c === 0x5be ||
2020 2
                $c === 0x5c0 ||
2021 2
                $c === 0x5c3 ||
2022 2
                $c === 0x5c6 ||
2023 2
                ($c >= 0x5d0 && $c <= 0x5ea) ||
2024 2
                ($c >= 0x5f0 && $c <= 0x5f4) ||
2025 2
                $c === 0x608 ||
2026 2
                $c === 0x60b ||
2027 2
                $c === 0x60d ||
2028 2
                $c === 0x61b ||
2029 2
                ($c >= 0x61e && $c <= 0x64a) ||
2030
                ($c >= 0x66d && $c <= 0x66f) ||
2031
                ($c >= 0x671 && $c <= 0x6d5) ||
2032
                ($c >= 0x6e5 && $c <= 0x6e6) ||
2033
                ($c >= 0x6ee && $c <= 0x6ef) ||
2034
                ($c >= 0x6fa && $c <= 0x70d) ||
2035
                $c === 0x710 ||
2036
                ($c >= 0x712 && $c <= 0x72f) ||
2037
                ($c >= 0x74d && $c <= 0x7a5) ||
2038
                $c === 0x7b1 ||
2039
                ($c >= 0x7c0 && $c <= 0x7ea) ||
2040
                ($c >= 0x7f4 && $c <= 0x7f5) ||
2041
                $c === 0x7fa ||
2042
                ($c >= 0x800 && $c <= 0x815) ||
2043
                $c === 0x81a ||
2044
                $c === 0x824 ||
2045
                $c === 0x828 ||
2046
                ($c >= 0x830 && $c <= 0x83e) ||
2047
                ($c >= 0x840 && $c <= 0x858) ||
2048 2
                $c === 0x85e
2049
            ) {
2050 2
                return 'RTL';
2051
            }
2052 2
        } elseif ($c === 0x200f) {
2053
            return 'RTL';
2054 2
        } elseif ($c >= 0xfb1d) {
2055 2
            if ($c === 0xfb1d ||
2056 2
                ($c >= 0xfb1f && $c <= 0xfb28) ||
2057 2
                ($c >= 0xfb2a && $c <= 0xfb36) ||
2058 2
                ($c >= 0xfb38 && $c <= 0xfb3c) ||
2059 2
                $c === 0xfb3e ||
2060 2
                ($c >= 0xfb40 && $c <= 0xfb41) ||
2061 2
                ($c >= 0xfb43 && $c <= 0xfb44) ||
2062 2
                ($c >= 0xfb46 && $c <= 0xfbc1) ||
2063 2
                ($c >= 0xfbd3 && $c <= 0xfd3d) ||
2064 2
                ($c >= 0xfd50 && $c <= 0xfd8f) ||
2065 2
                ($c >= 0xfd92 && $c <= 0xfdc7) ||
2066 2
                ($c >= 0xfdf0 && $c <= 0xfdfc) ||
2067 2
                ($c >= 0xfe70 && $c <= 0xfe74) ||
2068 2
                ($c >= 0xfe76 && $c <= 0xfefc) ||
2069 2
                ($c >= 0x10800 && $c <= 0x10805) ||
2070 2
                $c === 0x10808 ||
2071 2
                ($c >= 0x1080a && $c <= 0x10835) ||
2072 2
                ($c >= 0x10837 && $c <= 0x10838) ||
2073 2
                $c === 0x1083c ||
2074 2
                ($c >= 0x1083f && $c <= 0x10855) ||
2075 2
                ($c >= 0x10857 && $c <= 0x1085f) ||
2076 2
                ($c >= 0x10900 && $c <= 0x1091b) ||
2077 2
                ($c >= 0x10920 && $c <= 0x10939) ||
2078 2
                $c === 0x1093f ||
2079 2
                $c === 0x10a00 ||
2080 2
                ($c >= 0x10a10 && $c <= 0x10a13) ||
2081 2
                ($c >= 0x10a15 && $c <= 0x10a17) ||
2082 2
                ($c >= 0x10a19 && $c <= 0x10a33) ||
2083 2
                ($c >= 0x10a40 && $c <= 0x10a47) ||
2084 2
                ($c >= 0x10a50 && $c <= 0x10a58) ||
2085 2
                ($c >= 0x10a60 && $c <= 0x10a7f) ||
2086 2
                ($c >= 0x10b00 && $c <= 0x10b35) ||
2087 2
                ($c >= 0x10b40 && $c <= 0x10b55) ||
2088 2
                ($c >= 0x10b58 && $c <= 0x10b72) ||
2089 2
                ($c >= 0x10b78 && $c <= 0x10b7f)
2090
            ) {
2091 2
                return 'RTL';
2092
            }
2093
        }
2094
2095 2
        return 'LTR';
2096
    }
2097
2098
    /**
2099
     * Check for php-support.
2100
     *
2101
     * @param string|null $key
2102
     *
2103
     * @return mixed
2104
     *               Return the full support-"array", if $key === null<br>
2105
     *               return bool-value, if $key is used and available<br>
2106
     *               otherwise return <strong>null</strong>
2107
     */
2108 27
    public static function getSupportInfo(string $key = null)
2109
    {
2110 27
        if ($key === null) {
2111 4
            return self::$SUPPORT;
2112
        }
2113
2114 25
        if (self::$INTL_TRANSLITERATOR_LIST === null) {
2115 1
            self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
2116
        }
2117
        // compatibility fix for old versions
2118 25
        self::$SUPPORT['intl__transliterator_list_ids'] = self::$INTL_TRANSLITERATOR_LIST;
2119
2120 25
        return self::$SUPPORT[$key] ?? null;
2121
    }
2122
2123
    /**
2124
     * Warning: this method only works for some file-types (png, jpg)
2125
     *          if you need more supported types, please use e.g. "finfo"
2126
     *
2127
     * @param string $str
2128
     * @param array  $fallback with this keys: 'ext', 'mime', 'type'
2129
     *
2130
     * @return array
2131
     *               with this keys: 'ext', 'mime', 'type'
2132
     */
2133 39
    public static function get_file_type(
2134
        string $str,
2135
        array $fallback = [
2136
            'ext'  => null,
2137
            'mime' => 'application/octet-stream',
2138
            'type' => null,
2139
        ]
2140
    ): array {
2141 39
        if ($str === '') {
2142
            return $fallback;
2143
        }
2144
2145
        /** @var false|string $str_info - needed for PhpStan (stubs error) */
2146 39
        $str_info = \substr($str, 0, 2);
2147 39
        if ($str_info === false || \strlen($str_info) !== 2) {
2148 11
            return $fallback;
2149
        }
2150
2151
        // DEBUG
2152
        //var_dump($str_info);
2153
2154
        /** @var array|false $str_info - needed for PhpStan (stubs error) */
2155 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

2155
        $str_info = \unpack('C2chars', /** @scrutinizer ignore-type */ $str_info);
Loading history...
2156 35
        if ($str_info === false) {
2157
            return $fallback;
2158
        }
2159
        /** @noinspection OffsetOperationsInspection */
2160 35
        $type_code = (int) ($str_info['chars1'] . $str_info['chars2']);
2161
2162
        // DEBUG
2163
        //var_dump($type_code);
2164
2165
        //
2166
        // info: https://en.wikipedia.org/wiki/Magic_number_%28programming%29#Format_indicator
2167
        //
2168
        switch ($type_code) {
2169
            // WARNING: do not add too simple comparisons, because of false-positive results:
2170
            //
2171
            // 3780 => 'pdf', 7790 => 'exe', 7784 => 'midi', 8075 => 'zip',
2172
            // 8297 => 'rar', 7173 => 'gif', 7373 => 'tiff' 6677 => 'bmp', ...
2173
            //
2174 35
            case 255216:
2175
                $ext = 'jpg';
2176
                $mime = 'image/jpeg';
2177
                $type = 'binary';
2178
2179
                break;
2180 35
            case 13780:
2181 7
                $ext = 'png';
2182 7
                $mime = 'image/png';
2183 7
                $type = 'binary';
2184
2185 7
                break;
2186
            default:
2187 34
                return $fallback;
2188
        }
2189
2190
        return [
2191 7
            'ext'  => $ext,
2192 7
            'mime' => $mime,
2193 7
            'type' => $type,
2194
        ];
2195
    }
2196
2197
    /**
2198
     * @param int    $length         <p>Length of the random string.</p>
2199
     * @param string $possible_chars [optional] <p>Characters string for the random selection.</p>
2200
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
2201
     *
2202
     * @return string
2203
     */
2204 1
    public static function get_random_string(
2205
        int $length,
2206
        string $possible_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
2207
        string $encoding = 'UTF-8'
2208
    ): string {
2209
        // init
2210 1
        $i = 0;
2211 1
        $str = '';
2212
2213
        //
2214
        // add random chars
2215
        //
2216
2217 1
        if ($encoding === 'UTF-8') {
2218 1
            $max_length = (int) \mb_strlen($possible_chars);
2219 1
            if ($max_length === 0) {
2220 1
                return '';
2221
            }
2222
2223 1
            while ($i < $length) {
2224
                try {
2225 1
                    $rand_int = \random_int(0, $max_length - 1);
2226
                } catch (\Exception $e) {
2227
                    /** @noinspection RandomApiMigrationInspection */
2228
                    $rand_int = \mt_rand(0, $max_length - 1);
2229
                }
2230 1
                $char = \mb_substr($possible_chars, $rand_int, 1);
2231 1
                if ($char !== false) {
2232 1
                    $str .= $char;
2233 1
                    ++$i;
2234
                }
2235
            }
2236
        } else {
2237
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2238
2239
            $max_length = (int) self::strlen($possible_chars, $encoding);
2240
            if ($max_length === 0) {
2241
                return '';
2242
            }
2243
2244
            while ($i < $length) {
2245
                try {
2246
                    $rand_int = \random_int(0, $max_length - 1);
2247
                } catch (\Exception $e) {
2248
                    /** @noinspection RandomApiMigrationInspection */
2249
                    $rand_int = \mt_rand(0, $max_length - 1);
2250
                }
2251
                $char = self::substr($possible_chars, $rand_int, 1, $encoding);
2252
                if ($char !== false) {
2253
                    $str .= $char;
2254
                    ++$i;
2255
                }
2256
            }
2257
        }
2258
2259 1
        return $str;
2260
    }
2261
2262
    /**
2263
     * @param int|string $entropy_extra [optional] <p>Extra entropy via a string or int value.</p>
2264
     * @param bool       $use_md5       [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
2265
     *
2266
     * @return string
2267
     */
2268 1
    public static function get_unique_string($entropy_extra = '', bool $use_md5 = true): string
2269
    {
2270 1
        $unique_helper = \random_int(0, \mt_getrandmax()) .
2271 1
                        \session_id() .
2272 1
                        ($_SERVER['REMOTE_ADDR'] ?? '') .
2273 1
                        ($_SERVER['SERVER_ADDR'] ?? '') .
2274 1
                        $entropy_extra;
2275
2276 1
        $unique_string = \uniqid($unique_helper, true);
2277
2278 1
        if ($use_md5) {
2279 1
            $unique_string = \md5($unique_string . $unique_helper);
2280
        }
2281
2282 1
        return $unique_string;
2283
    }
2284
2285
    /**
2286
     * alias for "UTF8::string_has_bom()"
2287
     *
2288
     * @param string $str
2289
     *
2290
     * @return bool
2291
     *
2292
     * @see UTF8::string_has_bom()
2293
     * @deprecated <p>please use "UTF8::string_has_bom()"</p>
2294
     */
2295 2
    public static function hasBom(string $str): bool
2296
    {
2297 2
        return self::string_has_bom($str);
2298
    }
2299
2300
    /**
2301
     * Returns true if the string contains a lower case char, false otherwise.
2302
     *
2303
     * @param string $str <p>The input string.</p>
2304
     *
2305
     * @return bool whether or not the string contains a lower case character
2306
     */
2307 47
    public static function has_lowercase(string $str): bool
2308
    {
2309 47
        if (self::$SUPPORT['mbstring'] === true) {
2310
            /** @noinspection PhpComposerExtensionStubsInspection */
2311 47
            return \mb_ereg_match('.*[[:lower:]]', $str);
2312
        }
2313
2314
        return self::str_matches_pattern($str, '.*[[:lower:]]');
2315
    }
2316
2317
    /**
2318
     * Returns true if the string contains an upper case char, false otherwise.
2319
     *
2320
     * @param string $str <p>The input string.</p>
2321
     *
2322
     * @return bool whether or not the string contains an upper case character
2323
     */
2324 12
    public static function has_uppercase(string $str): bool
2325
    {
2326 12
        if (self::$SUPPORT['mbstring'] === true) {
2327
            /** @noinspection PhpComposerExtensionStubsInspection */
2328 12
            return \mb_ereg_match('.*[[:upper:]]', $str);
2329
        }
2330
2331
        return self::str_matches_pattern($str, '.*[[:upper:]]');
2332
    }
2333
2334
    /**
2335
     * Converts a hexadecimal value into a UTF-8 character.
2336
     *
2337
     * @param string $hexdec <p>The hexadecimal value.</p>
2338
     *
2339
     * @return false|string one single UTF-8 character
2340
     */
2341 4
    public static function hex_to_chr(string $hexdec)
2342
    {
2343 4
        return self::decimal_to_chr(\hexdec($hexdec));
2344
    }
2345
2346
    /**
2347
     * Converts hexadecimal U+xxxx code point representation to integer.
2348
     *
2349
     * INFO: opposite to UTF8::int_to_hex()
2350
     *
2351
     * @param string $hexdec <p>The hexadecimal code point representation.</p>
2352
     *
2353
     * @return false|int the code point, or false on failure
2354
     */
2355 2
    public static function hex_to_int($hexdec)
2356
    {
2357
        // init
2358 2
        $hexdec = (string) $hexdec;
2359
2360 2
        if ($hexdec === '') {
2361 2
            return false;
2362
        }
2363
2364 2
        if (\preg_match('/^(?:\\\u|U\+|)([a-zA-Z0-9]{4,6})$/', $hexdec, $match)) {
2365 2
            return \intval($match[1], 16);
2366
        }
2367
2368 2
        return false;
2369
    }
2370
2371
    /**
2372
     * alias for "UTF8::html_entity_decode()"
2373
     *
2374
     * @param string $str
2375
     * @param int    $flags
2376
     * @param string $encoding
2377
     *
2378
     * @return string
2379
     *
2380
     * @see UTF8::html_entity_decode()
2381
     * @deprecated <p>please use "UTF8::html_entity_decode()"</p>
2382
     */
2383 2
    public static function html_decode(
2384
        string $str,
2385
        int $flags = null,
2386
        string $encoding = 'UTF-8'
2387
    ): string {
2388 2
        return self::html_entity_decode($str, $flags, $encoding);
2389
    }
2390
2391
    /**
2392
     * Converts a UTF-8 string to a series of HTML numbered entities.
2393
     *
2394
     * INFO: opposite to UTF8::html_decode()
2395
     *
2396
     * @param string $str              <p>The Unicode string to be encoded as numbered entities.</p>
2397
     * @param bool   $keep_ascii_chars [optional] <p>Keep ASCII chars.</p>
2398
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
2399
     *
2400
     * @return string HTML numbered entities
2401
     */
2402 14
    public static function html_encode(
2403
        string $str,
2404
        bool $keep_ascii_chars = false,
2405
        string $encoding = 'UTF-8'
2406
    ): string {
2407 14
        if ($str === '') {
2408 4
            return '';
2409
        }
2410
2411 14
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2412 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2413
        }
2414
2415
        // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
2416 14
        if (self::$SUPPORT['mbstring'] === true) {
2417 14
            $start_code = 0x00;
2418 14
            if ($keep_ascii_chars === true) {
2419 13
                $start_code = 0x80;
2420
            }
2421
2422 14
            if ($encoding === 'UTF-8') {
2423
                /** @var false|string|null $return - needed for PhpStan (stubs error) */
2424 14
                $return = \mb_encode_numericentity(
2425 14
                    $str,
2426 14
                    [$start_code, 0xfffff, 0, 0xfffff, 0]
2427
                );
2428 14
                if ($return !== null && $return !== false) {
2429 14
                    return $return;
2430
                }
2431
            }
2432
2433
            /** @var false|string|null $return - needed for PhpStan (stubs error) */
2434 4
            $return = \mb_encode_numericentity(
2435 4
                $str,
2436 4
                [$start_code, 0xfffff, 0, 0xfffff, 0],
2437 4
                $encoding
2438
            );
2439 4
            if ($return !== null && $return !== false) {
2440 4
                return $return;
2441
            }
2442
        }
2443
2444
        //
2445
        // fallback via vanilla php
2446
        //
2447
2448
        return \implode(
2449
            '',
2450
            \array_map(
2451
                static function (string $chr) use ($keep_ascii_chars, $encoding): string {
2452
                    return self::single_chr_html_encode($chr, $keep_ascii_chars, $encoding);
2453
                },
2454
                self::str_split($str)
2455
            )
2456
        );
2457
    }
2458
2459
    /**
2460
     * UTF-8 version of html_entity_decode()
2461
     *
2462
     * The reason we are not using html_entity_decode() by itself is because
2463
     * while it is not technically correct to leave out the semicolon
2464
     * at the end of an entity most browsers will still interpret the entity
2465
     * correctly. html_entity_decode() does not convert entities without
2466
     * semicolons, so we are left with our own little solution here. Bummer.
2467
     *
2468
     * Convert all HTML entities to their applicable characters
2469
     *
2470
     * INFO: opposite to UTF8::html_encode()
2471
     *
2472
     * @see http://php.net/manual/en/function.html-entity-decode.php
2473
     *
2474
     * @param string $str      <p>
2475
     *                         The input string.
2476
     *                         </p>
2477
     * @param int    $flags    [optional] <p>
2478
     *                         A bitmask of one or more of the following flags, which specify how to handle quotes
2479
     *                         and which document type to use. The default is ENT_COMPAT | ENT_HTML401.
2480
     *                         <table>
2481
     *                         Available <i>flags</i> constants
2482
     *                         <tr valign="top">
2483
     *                         <td>Constant Name</td>
2484
     *                         <td>Description</td>
2485
     *                         </tr>
2486
     *                         <tr valign="top">
2487
     *                         <td><b>ENT_COMPAT</b></td>
2488
     *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
2489
     *                         </tr>
2490
     *                         <tr valign="top">
2491
     *                         <td><b>ENT_QUOTES</b></td>
2492
     *                         <td>Will convert both double and single quotes.</td>
2493
     *                         </tr>
2494
     *                         <tr valign="top">
2495
     *                         <td><b>ENT_NOQUOTES</b></td>
2496
     *                         <td>Will leave both double and single quotes unconverted.</td>
2497
     *                         </tr>
2498
     *                         <tr valign="top">
2499
     *                         <td><b>ENT_HTML401</b></td>
2500
     *                         <td>
2501
     *                         Handle code as HTML 4.01.
2502
     *                         </td>
2503
     *                         </tr>
2504
     *                         <tr valign="top">
2505
     *                         <td><b>ENT_XML1</b></td>
2506
     *                         <td>
2507
     *                         Handle code as XML 1.
2508
     *                         </td>
2509
     *                         </tr>
2510
     *                         <tr valign="top">
2511
     *                         <td><b>ENT_XHTML</b></td>
2512
     *                         <td>
2513
     *                         Handle code as XHTML.
2514
     *                         </td>
2515
     *                         </tr>
2516
     *                         <tr valign="top">
2517
     *                         <td><b>ENT_HTML5</b></td>
2518
     *                         <td>
2519
     *                         Handle code as HTML 5.
2520
     *                         </td>
2521
     *                         </tr>
2522
     *                         </table>
2523
     *                         </p>
2524
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2525
     *
2526
     * @return string the decoded string
2527
     */
2528 46
    public static function html_entity_decode(
2529
        string $str,
2530
        int $flags = null,
2531
        string $encoding = 'UTF-8'
2532
    ): string {
2533
        if (
2534 46
            !isset($str[3]) // examples: &; || &x;
2535
            ||
2536 46
            \strpos($str, '&') === false // no "&"
2537
        ) {
2538 23
            return $str;
2539
        }
2540
2541 44
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2542 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2543
        }
2544
2545 44
        if ($flags === null) {
2546 10
            $flags = \ENT_QUOTES | \ENT_HTML5;
2547
        }
2548
2549
        if (
2550 44
            $encoding !== 'UTF-8'
2551
            &&
2552 44
            $encoding !== 'ISO-8859-1'
2553
            &&
2554 44
            $encoding !== 'WINDOWS-1252'
2555
            &&
2556 44
            self::$SUPPORT['mbstring'] === false
2557
        ) {
2558
            \trigger_error('UTF8::html_entity_decode() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
2559
        }
2560
2561
        do {
2562 44
            $str_compare = $str;
2563
2564
            // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
2565 44
            if (self::$SUPPORT['mbstring'] === true) {
2566 44
                if ($encoding === 'UTF-8') {
2567
                    /** @var false|string|null $strTmp - needed for PhpStan (stubs error) */
2568 44
                    $strTmp = \mb_decode_numericentity(
2569 44
                        $str,
2570 44
                        [0x80, 0xfffff, 0, 0xfffff, 0]
2571
                    );
2572
                } else {
2573
                    /** @var false|string|null $strTmp - needed for PhpStan (stubs error) */
2574 4
                    $strTmp = \mb_decode_numericentity(
2575 4
                        $str,
2576 4
                        [0x80, 0xfffff, 0, 0xfffff, 0],
2577 4
                        $encoding
2578
                    );
2579
                }
2580 44
                if ($strTmp === null || $strTmp === false) {
2581 44
                    $str = self::html_entity_decode_helper($str, $encoding);
2582
                }
2583
            } else {
2584
                $str = self::html_entity_decode_helper($str, $encoding);
2585
            }
2586
2587 44
            if (\strpos($str, '&') !== false) {
2588 44
                if (\strpos($str, '&#') !== false) {
2589
                    // decode also numeric & UTF16 two byte entities
2590 36
                    $str = (string) \preg_replace(
2591 36
                        '/(&#(?:x0*[0-9a-fA-F]{2,6}(?![0-9a-fA-F;])|(?:0*\d{2,6}(?![0-9;]))))/S',
2592 36
                        '$1;',
2593 36
                        $str
2594
                    );
2595
                }
2596
2597 44
                $str = \html_entity_decode(
2598 44
                    $str,
2599 44
                    $flags,
2600 44
                    $encoding
2601
                );
2602
            }
2603 44
        } while ($str_compare !== $str);
2604
2605 44
        return $str;
2606
    }
2607
2608
    /**
2609
     * Create a escape html version of the string via "UTF8::htmlspecialchars()".
2610
     *
2611
     * @param string $str
2612
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2613
     *
2614
     * @return string
2615
     */
2616 6
    public static function html_escape(string $str, string $encoding = 'UTF-8'): string
2617
    {
2618 6
        return self::htmlspecialchars(
2619 6
            $str,
2620 6
            \ENT_QUOTES | \ENT_SUBSTITUTE,
2621 6
            $encoding
2622
        );
2623
    }
2624
2625
    /**
2626
     * Remove empty html-tag.
2627
     *
2628
     * e.g.: <tag></tag>
2629
     *
2630
     * @param string $str
2631
     *
2632
     * @return string
2633
     */
2634 1
    public static function html_stripe_empty_tags(string $str): string
2635
    {
2636 1
        return (string) \preg_replace(
2637 1
            '/<[^\\/>]*?>\\s*?<\\/[^>]*?>/u',
2638 1
            '',
2639 1
            $str
2640
        );
2641
    }
2642
2643
    /**
2644
     * Convert all applicable characters to HTML entities: UTF-8 version of htmlentities()
2645
     *
2646
     * @see http://php.net/manual/en/function.htmlentities.php
2647
     *
2648
     * @param string $str           <p>
2649
     *                              The input string.
2650
     *                              </p>
2651
     * @param int    $flags         [optional] <p>
2652
     *                              A bitmask of one or more of the following flags, which specify how to handle
2653
     *                              quotes, invalid code unit sequences and the used document type. The default is
2654
     *                              ENT_COMPAT | ENT_HTML401.
2655
     *                              <table>
2656
     *                              Available <i>flags</i> constants
2657
     *                              <tr valign="top">
2658
     *                              <td>Constant Name</td>
2659
     *                              <td>Description</td>
2660
     *                              </tr>
2661
     *                              <tr valign="top">
2662
     *                              <td><b>ENT_COMPAT</b></td>
2663
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2664
     *                              </tr>
2665
     *                              <tr valign="top">
2666
     *                              <td><b>ENT_QUOTES</b></td>
2667
     *                              <td>Will convert both double and single quotes.</td>
2668
     *                              </tr>
2669
     *                              <tr valign="top">
2670
     *                              <td><b>ENT_NOQUOTES</b></td>
2671
     *                              <td>Will leave both double and single quotes unconverted.</td>
2672
     *                              </tr>
2673
     *                              <tr valign="top">
2674
     *                              <td><b>ENT_IGNORE</b></td>
2675
     *                              <td>
2676
     *                              Silently discard invalid code unit sequences instead of returning
2677
     *                              an empty string. Using this flag is discouraged as it
2678
     *                              may have security implications.
2679
     *                              </td>
2680
     *                              </tr>
2681
     *                              <tr valign="top">
2682
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2683
     *                              <td>
2684
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
2685
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
2686
     *                              string.
2687
     *                              </td>
2688
     *                              </tr>
2689
     *                              <tr valign="top">
2690
     *                              <td><b>ENT_DISALLOWED</b></td>
2691
     *                              <td>
2692
     *                              Replace invalid code points for the given document type with a
2693
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
2694
     *                              (otherwise) instead of leaving them as is. This may be useful, for
2695
     *                              instance, to ensure the well-formedness of XML documents with
2696
     *                              embedded external content.
2697
     *                              </td>
2698
     *                              </tr>
2699
     *                              <tr valign="top">
2700
     *                              <td><b>ENT_HTML401</b></td>
2701
     *                              <td>
2702
     *                              Handle code as HTML 4.01.
2703
     *                              </td>
2704
     *                              </tr>
2705
     *                              <tr valign="top">
2706
     *                              <td><b>ENT_XML1</b></td>
2707
     *                              <td>
2708
     *                              Handle code as XML 1.
2709
     *                              </td>
2710
     *                              </tr>
2711
     *                              <tr valign="top">
2712
     *                              <td><b>ENT_XHTML</b></td>
2713
     *                              <td>
2714
     *                              Handle code as XHTML.
2715
     *                              </td>
2716
     *                              </tr>
2717
     *                              <tr valign="top">
2718
     *                              <td><b>ENT_HTML5</b></td>
2719
     *                              <td>
2720
     *                              Handle code as HTML 5.
2721
     *                              </td>
2722
     *                              </tr>
2723
     *                              </table>
2724
     *                              </p>
2725
     * @param string $encoding      [optional] <p>
2726
     *                              Like <b>htmlspecialchars</b>,
2727
     *                              <b>htmlentities</b> takes an optional third argument
2728
     *                              <i>encoding</i> which defines encoding used in
2729
     *                              conversion.
2730
     *                              Although this argument is technically optional, you are highly
2731
     *                              encouraged to specify the correct value for your code.
2732
     *                              </p>
2733
     * @param bool   $double_encode [optional] <p>
2734
     *                              When <i>double_encode</i> is turned off PHP will not
2735
     *                              encode existing html entities. The default is to convert everything.
2736
     *                              </p>
2737
     *
2738
     * @return string
2739
     *                <p>
2740
     *                The encoded string.
2741
     *                <br><br>
2742
     *                If the input <i>string</i> contains an invalid code unit
2743
     *                sequence within the given <i>encoding</i> an empty string
2744
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
2745
     *                <b>ENT_SUBSTITUTE</b> flags are set.
2746
     *                </p>
2747
     */
2748 9
    public static function htmlentities(
2749
        string $str,
2750
        int $flags = \ENT_COMPAT,
2751
        string $encoding = 'UTF-8',
2752
        bool $double_encode = true
2753
    ): string {
2754 9
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2755 7
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2756
        }
2757
2758 9
        $str = \htmlentities(
2759 9
            $str,
2760 9
            $flags,
2761 9
            $encoding,
2762 9
            $double_encode
2763
        );
2764
2765
        /**
2766
         * PHP doesn't replace a backslash to its html entity since this is something
2767
         * that's mostly used to escape characters when inserting in a database. Since
2768
         * we're using a decent database layer, we don't need this shit and we're replacing
2769
         * the double backslashes by its' html entity equivalent.
2770
         *
2771
         * https://github.com/forkcms/library/blob/master/spoon/filter/filter.php#L303
2772
         */
2773 9
        $str = \str_replace('\\', '&#92;', $str);
2774
2775 9
        return self::html_encode($str, true, $encoding);
2776
    }
2777
2778
    /**
2779
     * Convert only special characters to HTML entities: UTF-8 version of htmlspecialchars()
2780
     *
2781
     * INFO: Take a look at "UTF8::htmlentities()"
2782
     *
2783
     * @see http://php.net/manual/en/function.htmlspecialchars.php
2784
     *
2785
     * @param string $str           <p>
2786
     *                              The string being converted.
2787
     *                              </p>
2788
     * @param int    $flags         [optional] <p>
2789
     *                              A bitmask of one or more of the following flags, which specify how to handle
2790
     *                              quotes, invalid code unit sequences and the used document type. The default is
2791
     *                              ENT_COMPAT | ENT_HTML401.
2792
     *                              <table>
2793
     *                              Available <i>flags</i> constants
2794
     *                              <tr valign="top">
2795
     *                              <td>Constant Name</td>
2796
     *                              <td>Description</td>
2797
     *                              </tr>
2798
     *                              <tr valign="top">
2799
     *                              <td><b>ENT_COMPAT</b></td>
2800
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2801
     *                              </tr>
2802
     *                              <tr valign="top">
2803
     *                              <td><b>ENT_QUOTES</b></td>
2804
     *                              <td>Will convert both double and single quotes.</td>
2805
     *                              </tr>
2806
     *                              <tr valign="top">
2807
     *                              <td><b>ENT_NOQUOTES</b></td>
2808
     *                              <td>Will leave both double and single quotes unconverted.</td>
2809
     *                              </tr>
2810
     *                              <tr valign="top">
2811
     *                              <td><b>ENT_IGNORE</b></td>
2812
     *                              <td>
2813
     *                              Silently discard invalid code unit sequences instead of returning
2814
     *                              an empty string. Using this flag is discouraged as it
2815
     *                              may have security implications.
2816
     *                              </td>
2817
     *                              </tr>
2818
     *                              <tr valign="top">
2819
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2820
     *                              <td>
2821
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
2822
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
2823
     *                              string.
2824
     *                              </td>
2825
     *                              </tr>
2826
     *                              <tr valign="top">
2827
     *                              <td><b>ENT_DISALLOWED</b></td>
2828
     *                              <td>
2829
     *                              Replace invalid code points for the given document type with a
2830
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
2831
     *                              (otherwise) instead of leaving them as is. This may be useful, for
2832
     *                              instance, to ensure the well-formedness of XML documents with
2833
     *                              embedded external content.
2834
     *                              </td>
2835
     *                              </tr>
2836
     *                              <tr valign="top">
2837
     *                              <td><b>ENT_HTML401</b></td>
2838
     *                              <td>
2839
     *                              Handle code as HTML 4.01.
2840
     *                              </td>
2841
     *                              </tr>
2842
     *                              <tr valign="top">
2843
     *                              <td><b>ENT_XML1</b></td>
2844
     *                              <td>
2845
     *                              Handle code as XML 1.
2846
     *                              </td>
2847
     *                              </tr>
2848
     *                              <tr valign="top">
2849
     *                              <td><b>ENT_XHTML</b></td>
2850
     *                              <td>
2851
     *                              Handle code as XHTML.
2852
     *                              </td>
2853
     *                              </tr>
2854
     *                              <tr valign="top">
2855
     *                              <td><b>ENT_HTML5</b></td>
2856
     *                              <td>
2857
     *                              Handle code as HTML 5.
2858
     *                              </td>
2859
     *                              </tr>
2860
     *                              </table>
2861
     *                              </p>
2862
     * @param string $encoding      [optional] <p>
2863
     *                              Defines encoding used in conversion.
2864
     *                              </p>
2865
     *                              <p>
2866
     *                              For the purposes of this function, the encodings
2867
     *                              ISO-8859-1, ISO-8859-15,
2868
     *                              UTF-8, cp866,
2869
     *                              cp1251, cp1252, and
2870
     *                              KOI8-R are effectively equivalent, provided the
2871
     *                              <i>string</i> itself is valid for the encoding, as
2872
     *                              the characters affected by <b>htmlspecialchars</b> occupy
2873
     *                              the same positions in all of these encodings.
2874
     *                              </p>
2875
     * @param bool   $double_encode [optional] <p>
2876
     *                              When <i>double_encode</i> is turned off PHP will not
2877
     *                              encode existing html entities, the default is to convert everything.
2878
     *                              </p>
2879
     *
2880
     * @return string the converted string.
2881
     *                </p>
2882
     *                <p>
2883
     *                If the input <i>string</i> contains an invalid code unit
2884
     *                sequence within the given <i>encoding</i> an empty string
2885
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
2886
     *                <b>ENT_SUBSTITUTE</b> flags are set
2887
     */
2888 8
    public static function htmlspecialchars(
2889
        string $str,
2890
        int $flags = \ENT_COMPAT,
2891
        string $encoding = 'UTF-8',
2892
        bool $double_encode = true
2893
    ): string {
2894 8
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2895 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2896
        }
2897
2898 8
        return \htmlspecialchars(
2899 8
            $str,
2900 8
            $flags,
2901 8
            $encoding,
2902 8
            $double_encode
2903
        );
2904
    }
2905
2906
    /**
2907
     * Checks whether iconv is available on the server.
2908
     *
2909
     * @return bool
2910
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
2911
     */
2912
    public static function iconv_loaded(): bool
2913
    {
2914
        return \extension_loaded('iconv');
2915
    }
2916
2917
    /**
2918
     * alias for "UTF8::decimal_to_chr()"
2919
     *
2920
     * @param mixed $int
2921
     *
2922
     * @return string
2923
     *
2924
     * @see UTF8::decimal_to_chr()
2925
     * @deprecated <p>please use "UTF8::decimal_to_chr()"</p>
2926
     */
2927 4
    public static function int_to_chr($int): string
2928
    {
2929 4
        return self::decimal_to_chr($int);
2930
    }
2931
2932
    /**
2933
     * Converts Integer to hexadecimal U+xxxx code point representation.
2934
     *
2935
     * INFO: opposite to UTF8::hex_to_int()
2936
     *
2937
     * @param int    $int    <p>The integer to be converted to hexadecimal code point.</p>
2938
     * @param string $prefix [optional]
2939
     *
2940
     * @return string the code point, or empty string on failure
2941
     */
2942 6
    public static function int_to_hex(int $int, string $prefix = 'U+'): string
2943
    {
2944 6
        $hex = \dechex($int);
2945
2946 6
        $hex = (\strlen($hex) < 4 ? \substr('0000' . $hex, -4) : $hex);
2947
2948 6
        return $prefix . $hex . '';
2949
    }
2950
2951
    /**
2952
     * Checks whether intl-char is available on the server.
2953
     *
2954
     * @return bool
2955
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
2956
     */
2957
    public static function intlChar_loaded(): bool
2958
    {
2959
        return \class_exists('IntlChar');
2960
    }
2961
2962
    /**
2963
     * Checks whether intl is available on the server.
2964
     *
2965
     * @return bool
2966
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
2967
     */
2968 5
    public static function intl_loaded(): bool
2969
    {
2970 5
        return \extension_loaded('intl');
2971
    }
2972
2973
    /**
2974
     * alias for "UTF8::is_ascii()"
2975
     *
2976
     * @param string $str
2977
     *
2978
     * @return bool
2979
     *
2980
     * @see UTF8::is_ascii()
2981
     * @deprecated <p>please use "UTF8::is_ascii()"</p>
2982
     */
2983 2
    public static function isAscii(string $str): bool
2984
    {
2985 2
        return ASCII::is_ascii($str);
2986
    }
2987
2988
    /**
2989
     * alias for "UTF8::is_base64()"
2990
     *
2991
     * @param string $str
2992
     *
2993
     * @return bool
2994
     *
2995
     * @see UTF8::is_base64()
2996
     * @deprecated <p>please use "UTF8::is_base64()"</p>
2997
     */
2998 2
    public static function isBase64($str): bool
2999
    {
3000 2
        return self::is_base64($str);
3001
    }
3002
3003
    /**
3004
     * alias for "UTF8::is_binary()"
3005
     *
3006
     * @param mixed $str
3007
     * @param bool  $strict
3008
     *
3009
     * @return bool
3010
     *
3011
     * @see UTF8::is_binary()
3012
     * @deprecated <p>please use "UTF8::is_binary()"</p>
3013
     */
3014 4
    public static function isBinary($str, $strict = false): bool
3015
    {
3016 4
        return self::is_binary($str, $strict);
3017
    }
3018
3019
    /**
3020
     * alias for "UTF8::is_bom()"
3021
     *
3022
     * @param string $utf8_chr
3023
     *
3024
     * @return bool
3025
     *
3026
     * @see UTF8::is_bom()
3027
     * @deprecated <p>please use "UTF8::is_bom()"</p>
3028
     */
3029 2
    public static function isBom(string $utf8_chr): bool
3030
    {
3031 2
        return self::is_bom($utf8_chr);
3032
    }
3033
3034
    /**
3035
     * alias for "UTF8::is_html()"
3036
     *
3037
     * @param string $str
3038
     *
3039
     * @return bool
3040
     *
3041
     * @see UTF8::is_html()
3042
     * @deprecated <p>please use "UTF8::is_html()"</p>
3043
     */
3044 2
    public static function isHtml(string $str): bool
3045
    {
3046 2
        return self::is_html($str);
3047
    }
3048
3049
    /**
3050
     * alias for "UTF8::is_json()"
3051
     *
3052
     * @param string $str
3053
     *
3054
     * @return bool
3055
     *
3056
     * @see UTF8::is_json()
3057
     * @deprecated <p>please use "UTF8::is_json()"</p>
3058
     */
3059
    public static function isJson(string $str): bool
3060
    {
3061
        return self::is_json($str);
3062
    }
3063
3064
    /**
3065
     * alias for "UTF8::is_utf16()"
3066
     *
3067
     * @param mixed $str
3068
     *
3069
     * @return false|int
3070
     *                   <strong>false</strong> if is't not UTF16,<br>
3071
     *                   <strong>1</strong> for UTF-16LE,<br>
3072
     *                   <strong>2</strong> for UTF-16BE
3073
     *
3074
     * @see UTF8::is_utf16()
3075
     * @deprecated <p>please use "UTF8::is_utf16()"</p>
3076
     */
3077 2
    public static function isUtf16($str)
3078
    {
3079 2
        return self::is_utf16($str);
3080
    }
3081
3082
    /**
3083
     * alias for "UTF8::is_utf32()"
3084
     *
3085
     * @param mixed $str
3086
     *
3087
     * @return false|int
3088
     *                   <strong>false</strong> if is't not UTF16,
3089
     *                   <strong>1</strong> for UTF-32LE,
3090
     *                   <strong>2</strong> for UTF-32BE
3091
     *
3092
     * @see UTF8::is_utf32()
3093
     * @deprecated <p>please use "UTF8::is_utf32()"</p>
3094
     */
3095 2
    public static function isUtf32($str)
3096
    {
3097 2
        return self::is_utf32($str);
3098
    }
3099
3100
    /**
3101
     * alias for "UTF8::is_utf8()"
3102
     *
3103
     * @param string $str
3104
     * @param bool   $strict
3105
     *
3106
     * @return bool
3107
     *
3108
     * @see UTF8::is_utf8()
3109
     * @deprecated <p>please use "UTF8::is_utf8()"</p>
3110
     */
3111 17
    public static function isUtf8($str, $strict = false): bool
3112
    {
3113 17
        return self::is_utf8($str, $strict);
3114
    }
3115
3116
    /**
3117
     * Returns true if the string contains only alphabetic chars, false otherwise.
3118
     *
3119
     * @param string $str
3120
     *
3121
     * @return bool
3122
     *              Whether or not $str contains only alphabetic chars
3123
     */
3124 10
    public static function is_alpha(string $str): bool
3125
    {
3126 10
        if (self::$SUPPORT['mbstring'] === true) {
3127
            /** @noinspection PhpComposerExtensionStubsInspection */
3128 10
            return \mb_ereg_match('^[[:alpha:]]*$', $str);
3129
        }
3130
3131
        return self::str_matches_pattern($str, '^[[:alpha:]]*$');
3132
    }
3133
3134
    /**
3135
     * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
3136
     *
3137
     * @param string $str
3138
     *
3139
     * @return bool
3140
     *              Whether or not $str contains only alphanumeric chars
3141
     */
3142 13
    public static function is_alphanumeric(string $str): bool
3143
    {
3144 13
        if (self::$SUPPORT['mbstring'] === true) {
3145
            /** @noinspection PhpComposerExtensionStubsInspection */
3146 13
            return \mb_ereg_match('^[[:alnum:]]*$', $str);
3147
        }
3148
3149
        return self::str_matches_pattern($str, '^[[:alnum:]]*$');
3150
    }
3151
3152
    /**
3153
     * Checks if a string is 7 bit ASCII.
3154
     *
3155
     * @param string $str <p>The string to check.</p>
3156
     *
3157
     * @return bool
3158
     *              <strong>true</strong> if it is ASCII<br>
3159
     *              <strong>false</strong> otherwise
3160
     */
3161 8
    public static function is_ascii(string $str): bool
3162
    {
3163 8
        return ASCII::is_ascii($str);
3164
    }
3165
3166
    /**
3167
     * Returns true if the string is base64 encoded, false otherwise.
3168
     *
3169
     * @param mixed|string $str                   <p>The input string.</p>
3170
     * @param bool         $empty_string_is_valid [optional] <p>Is an empty string valid base64 or not?</p>
3171
     *
3172
     * @return bool whether or not $str is base64 encoded
3173
     */
3174 16
    public static function is_base64($str, $empty_string_is_valid = false): bool
3175
    {
3176
        if (
3177 16
            $empty_string_is_valid === false
3178
            &&
3179 16
            $str === ''
3180
        ) {
3181 3
            return false;
3182
        }
3183
3184
        /**
3185
         * @psalm-suppress RedundantConditionGivenDocblockType
3186
         */
3187 15
        if (\is_string($str) === false) {
3188 2
            return false;
3189
        }
3190
3191 15
        $base64String = \base64_decode($str, true);
3192
3193 15
        return $base64String !== false && \base64_encode($base64String) === $str;
3194
    }
3195
3196
    /**
3197
     * Check if the input is binary... (is look like a hack).
3198
     *
3199
     * @param mixed $input
3200
     * @param bool  $strict
3201
     *
3202
     * @return bool
3203
     */
3204 39
    public static function is_binary($input, bool $strict = false): bool
3205
    {
3206 39
        $input = (string) $input;
3207 39
        if ($input === '') {
3208 10
            return false;
3209
        }
3210
3211 39
        if (\preg_match('~^[01]+$~', $input)) {
3212 13
            return true;
3213
        }
3214
3215 39
        $ext = self::get_file_type($input);
3216 39
        if ($ext['type'] === 'binary') {
3217 7
            return true;
3218
        }
3219
3220 38
        $test_length = \strlen($input);
3221 38
        $test_null_counting = \substr_count($input, "\x0", 0, $test_length);
3222 38
        if (($test_null_counting / $test_length) > 0.25) {
3223 15
            return true;
3224
        }
3225
3226 34
        if ($strict === true) {
3227 34
            if (self::$SUPPORT['finfo'] === false) {
3228
                throw new \RuntimeException('ext-fileinfo: is not installed');
3229
            }
3230
3231
            /** @noinspection PhpComposerExtensionStubsInspection */
3232 34
            $finfo_encoding = (new \finfo(\FILEINFO_MIME_ENCODING))->buffer($input);
3233 34
            if ($finfo_encoding && $finfo_encoding === 'binary') {
3234 15
                return true;
3235
            }
3236
        }
3237
3238 30
        return false;
3239
    }
3240
3241
    /**
3242
     * Check if the file is binary.
3243
     *
3244
     * @param string $file
3245
     *
3246
     * @return bool
3247
     */
3248 6
    public static function is_binary_file($file): bool
3249
    {
3250
        // init
3251 6
        $block = '';
3252
3253 6
        $fp = \fopen($file, 'rb');
3254 6
        if (\is_resource($fp)) {
3255 6
            $block = \fread($fp, 512);
3256 6
            \fclose($fp);
3257
        }
3258
3259 6
        if ($block === '') {
3260 2
            return false;
3261
        }
3262
3263 6
        return self::is_binary($block, true);
3264
    }
3265
3266
    /**
3267
     * Returns true if the string contains only whitespace chars, false otherwise.
3268
     *
3269
     * @param string $str
3270
     *
3271
     * @return bool
3272
     *              Whether or not $str contains only whitespace characters
3273
     */
3274 15
    public static function is_blank(string $str): bool
3275
    {
3276 15
        if (self::$SUPPORT['mbstring'] === true) {
3277
            /** @noinspection PhpComposerExtensionStubsInspection */
3278 15
            return \mb_ereg_match('^[[:space:]]*$', $str);
3279
        }
3280
3281
        return self::str_matches_pattern($str, '^[[:space:]]*$');
3282
    }
3283
3284
    /**
3285
     * Checks if the given string is equal to any "Byte Order Mark".
3286
     *
3287
     * WARNING: Use "UTF8::string_has_bom()" if you will check BOM in a string.
3288
     *
3289
     * @param string $str <p>The input string.</p>
3290
     *
3291
     * @return bool
3292
     *              <strong>true</strong> if the $utf8_chr is Byte Order Mark, <strong>false</strong> otherwise
3293
     */
3294 2
    public static function is_bom($str): bool
3295
    {
3296
        /** @noinspection PhpUnusedLocalVariableInspection */
3297 2
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
3298 2
            if ($str === $bom_string) {
3299 2
                return true;
3300
            }
3301
        }
3302
3303 2
        return false;
3304
    }
3305
3306
    /**
3307
     * Determine whether the string is considered to be empty.
3308
     *
3309
     * A variable is considered empty if it does not exist or if its value equals FALSE.
3310
     * empty() does not generate a warning if the variable does not exist.
3311
     *
3312
     * @param mixed $str
3313
     *
3314
     * @return bool whether or not $str is empty()
3315
     */
3316
    public static function is_empty($str): bool
3317
    {
3318
        return empty($str);
3319
    }
3320
3321
    /**
3322
     * Returns true if the string contains only hexadecimal chars, false otherwise.
3323
     *
3324
     * @param string $str
3325
     *
3326
     * @return bool
3327
     *              Whether or not $str contains only hexadecimal chars
3328
     */
3329 13
    public static function is_hexadecimal(string $str): bool
3330
    {
3331 13
        if (self::$SUPPORT['mbstring'] === true) {
3332
            /** @noinspection PhpComposerExtensionStubsInspection */
3333 13
            return \mb_ereg_match('^[[:xdigit:]]*$', $str);
3334
        }
3335
3336
        return self::str_matches_pattern($str, '^[[:xdigit:]]*$');
3337
    }
3338
3339
    /**
3340
     * Check if the string contains any HTML tags.
3341
     *
3342
     * @param string $str <p>The input string.</p>
3343
     *
3344
     * @return bool
3345
     */
3346 3
    public static function is_html(string $str): bool
3347
    {
3348 3
        if ($str === '') {
3349 3
            return false;
3350
        }
3351
3352
        // init
3353 3
        $matches = [];
3354
3355 3
        $str = self::emoji_encode($str); // hack for emoji support :/
3356
3357 3
        \preg_match("/<\\/?\\w+(?:(?:\\s+\\w+(?:\\s*=\\s*(?:\".*?\"|'.*?'|[^'\">\\s]+))?)*\\s*|\\s*)\\/?>/u", $str, $matches);
3358
3359 3
        return $matches !== [];
3360
    }
3361
3362
    /**
3363
     * Try to check if "$str" is a JSON-string.
3364
     *
3365
     * @param string $str                                    <p>The input string.</p>
3366
     * @param bool   $only_array_or_object_results_are_valid [optional] <p>Only array and objects are valid json results.</p>
3367
     *
3368
     * @return bool
3369
     */
3370 42
    public static function is_json(
3371
        string $str,
3372
        $only_array_or_object_results_are_valid = true
3373
    ): bool {
3374 42
        if ($str === '') {
3375 4
            return false;
3376
        }
3377
3378 40
        if (self::$SUPPORT['json'] === false) {
3379
            throw new \RuntimeException('ext-json: is not installed');
3380
        }
3381
3382 40
        $json = self::json_decode($str);
3383 40
        if ($json === null && \strtoupper($str) !== 'NULL') {
3384 18
            return false;
3385
        }
3386
3387
        if (
3388 24
            $only_array_or_object_results_are_valid === true
3389
            &&
3390 24
            \is_object($json) === false
3391
            &&
3392 24
            \is_array($json) === false
3393
        ) {
3394 5
            return false;
3395
        }
3396
3397
        /** @noinspection PhpComposerExtensionStubsInspection */
3398 19
        return \json_last_error() === \JSON_ERROR_NONE;
3399
    }
3400
3401
    /**
3402
     * @param string $str
3403
     *
3404
     * @return bool
3405
     */
3406 8
    public static function is_lowercase(string $str): bool
3407
    {
3408 8
        if (self::$SUPPORT['mbstring'] === true) {
3409
            /** @noinspection PhpComposerExtensionStubsInspection */
3410 8
            return \mb_ereg_match('^[[:lower:]]*$', $str);
3411
        }
3412
3413
        return self::str_matches_pattern($str, '^[[:lower:]]*$');
3414
    }
3415
3416
    /**
3417
     * Returns true if the string is serialized, false otherwise.
3418
     *
3419
     * @param string $str
3420
     *
3421
     * @return bool whether or not $str is serialized
3422
     */
3423 7
    public static function is_serialized(string $str): bool
3424
    {
3425 7
        if ($str === '') {
3426 1
            return false;
3427
        }
3428
3429
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
3430
        /** @noinspection UnserializeExploitsInspection */
3431 6
        return $str === 'b:0;'
3432
               ||
3433 6
               @\unserialize($str) !== false;
3434
    }
3435
3436
    /**
3437
     * Returns true if the string contains only lower case chars, false
3438
     * otherwise.
3439
     *
3440
     * @param string $str <p>The input string.</p>
3441
     *
3442
     * @return bool
3443
     *              <p>Whether or not $str contains only lower case characters.</p>
3444
     */
3445 8
    public static function is_uppercase(string $str): bool
3446
    {
3447 8
        if (self::$SUPPORT['mbstring'] === true) {
3448
            /** @noinspection PhpComposerExtensionStubsInspection */
3449 8
            return \mb_ereg_match('^[[:upper:]]*$', $str);
3450
        }
3451
3452
        return self::str_matches_pattern($str, '^[[:upper:]]*$');
3453
    }
3454
3455
    /**
3456
     * Check if the string is UTF-16.
3457
     *
3458
     * @param mixed $str                       <p>The input string.</p>
3459
     * @param bool  $check_if_string_is_binary
3460
     *
3461
     * @return false|int
3462
     *                   <strong>false</strong> if is't not UTF-16,<br>
3463
     *                   <strong>1</strong> for UTF-16LE,<br>
3464
     *                   <strong>2</strong> for UTF-16BE
3465
     */
3466 22
    public static function is_utf16($str, $check_if_string_is_binary = true)
3467
    {
3468
        // init
3469 22
        $str = (string) $str;
3470 22
        $str_chars = [];
3471
3472
        if (
3473 22
            $check_if_string_is_binary === true
3474
            &&
3475 22
            self::is_binary($str, true) === false
3476
        ) {
3477 2
            return false;
3478
        }
3479
3480 22
        if (self::$SUPPORT['mbstring'] === false) {
3481 3
            \trigger_error('UTF8::is_utf16() without mbstring may did not work correctly', \E_USER_WARNING);
3482
        }
3483
3484 22
        $str = self::remove_bom($str);
3485
3486 22
        $maybe_utf16le = 0;
3487 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16LE');
3488 22
        if ($test) {
3489 15
            $test2 = \mb_convert_encoding($test, 'UTF-16LE', 'UTF-8');
3490 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16LE');
3491 15
            if ($test3 === $test) {
3492 15
                if ($str_chars === []) {
3493 15
                    $str_chars = self::count_chars($str, true, false);
3494
                }
3495 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...
3496 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3497 15
                        ++$maybe_utf16le;
3498
                    }
3499
                }
3500 15
                unset($test3charEmpty);
3501
            }
3502
        }
3503
3504 22
        $maybe_utf16be = 0;
3505 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16BE');
3506 22
        if ($test) {
3507 15
            $test2 = \mb_convert_encoding($test, 'UTF-16BE', 'UTF-8');
3508 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16BE');
3509 15
            if ($test3 === $test) {
3510 15
                if ($str_chars === []) {
3511 7
                    $str_chars = self::count_chars($str, true, false);
3512
                }
3513 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...
3514 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3515 15
                        ++$maybe_utf16be;
3516
                    }
3517
                }
3518 15
                unset($test3charEmpty);
3519
            }
3520
        }
3521
3522 22
        if ($maybe_utf16be !== $maybe_utf16le) {
3523 7
            if ($maybe_utf16le > $maybe_utf16be) {
3524 5
                return 1;
3525
            }
3526
3527 6
            return 2;
3528
        }
3529
3530 18
        return false;
3531
    }
3532
3533
    /**
3534
     * Check if the string is UTF-32.
3535
     *
3536
     * @param mixed $str                       <p>The input string.</p>
3537
     * @param bool  $check_if_string_is_binary
3538
     *
3539
     * @return false|int
3540
     *                   <strong>false</strong> if is't not UTF-32,<br>
3541
     *                   <strong>1</strong> for UTF-32LE,<br>
3542
     *                   <strong>2</strong> for UTF-32BE
3543
     */
3544 20
    public static function is_utf32($str, $check_if_string_is_binary = true)
3545
    {
3546
        // init
3547 20
        $str = (string) $str;
3548 20
        $str_chars = [];
3549
3550
        if (
3551 20
            $check_if_string_is_binary === true
3552
            &&
3553 20
            self::is_binary($str, true) === false
3554
        ) {
3555 2
            return false;
3556
        }
3557
3558 20
        if (self::$SUPPORT['mbstring'] === false) {
3559 3
            \trigger_error('UTF8::is_utf32() without mbstring may did not work correctly', \E_USER_WARNING);
3560
        }
3561
3562 20
        $str = self::remove_bom($str);
3563
3564 20
        $maybe_utf32le = 0;
3565 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32LE');
3566 20
        if ($test) {
3567 13
            $test2 = \mb_convert_encoding($test, 'UTF-32LE', 'UTF-8');
3568 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32LE');
3569 13
            if ($test3 === $test) {
3570 13
                if ($str_chars === []) {
3571 13
                    $str_chars = self::count_chars($str, true, false);
3572
                }
3573 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...
3574 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3575 13
                        ++$maybe_utf32le;
3576
                    }
3577
                }
3578 13
                unset($test3charEmpty);
3579
            }
3580
        }
3581
3582 20
        $maybe_utf32be = 0;
3583 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32BE');
3584 20
        if ($test) {
3585 13
            $test2 = \mb_convert_encoding($test, 'UTF-32BE', 'UTF-8');
3586 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32BE');
3587 13
            if ($test3 === $test) {
3588 13
                if ($str_chars === []) {
3589 7
                    $str_chars = self::count_chars($str, true, false);
3590
                }
3591 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...
3592 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3593 13
                        ++$maybe_utf32be;
3594
                    }
3595
                }
3596 13
                unset($test3charEmpty);
3597
            }
3598
        }
3599
3600 20
        if ($maybe_utf32be !== $maybe_utf32le) {
3601 3
            if ($maybe_utf32le > $maybe_utf32be) {
3602 2
                return 1;
3603
            }
3604
3605 3
            return 2;
3606
        }
3607
3608 20
        return false;
3609
    }
3610
3611
    /**
3612
     * Checks whether the passed input contains only byte sequences that appear valid UTF-8.
3613
     *
3614
     * @param int|string|string[]|null $str    <p>The input to be checked.</p>
3615
     * @param bool                     $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
3616
     *
3617
     * @return bool
3618
     */
3619 82
    public static function is_utf8($str, bool $strict = false): bool
3620
    {
3621 82
        if (\is_array($str) === true) {
3622 2
            foreach ($str as &$v) {
3623 2
                if (self::is_utf8($v, $strict) === false) {
3624 2
                    return false;
3625
                }
3626
            }
3627
3628
            return true;
3629
        }
3630
3631 82
        return self::is_utf8_string((string) $str, $strict);
3632
    }
3633
3634
    /**
3635
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
3636
     * Decodes a JSON string
3637
     *
3638
     * @see http://php.net/manual/en/function.json-decode.php
3639
     *
3640
     * @param string $json    <p>
3641
     *                        The <i>json</i> string being decoded.
3642
     *                        </p>
3643
     *                        <p>
3644
     *                        This function only works with UTF-8 encoded strings.
3645
     *                        </p>
3646
     *                        <p>PHP implements a superset of
3647
     *                        JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
3648
     *                        only supports these values when they are nested inside an array or an object.
3649
     *                        </p>
3650
     * @param bool   $assoc   [optional] <p>
3651
     *                        When <b>TRUE</b>, returned objects will be converted into
3652
     *                        associative arrays.
3653
     *                        </p>
3654
     * @param int    $depth   [optional] <p>
3655
     *                        User specified recursion depth.
3656
     *                        </p>
3657
     * @param int    $options [optional] <p>
3658
     *                        Bitmask of JSON decode options. Currently only
3659
     *                        <b>JSON_BIGINT_AS_STRING</b>
3660
     *                        is supported (default is to cast large integers as floats)
3661
     *                        </p>
3662
     *
3663
     * @return mixed
3664
     *               The value encoded in <i>json</i> in appropriate PHP type. Values true, false and
3665
     *               null (case-insensitive) are returned as <b>TRUE</b>, <b>FALSE</b> and <b>NULL</b> respectively.
3666
     *               <b>NULL</b> is returned if the <i>json</i> cannot be decoded or if the encoded data
3667
     *               is deeper than the recursion limit.
3668
     */
3669 43
    public static function json_decode(
3670
        string $json,
3671
        bool $assoc = false,
3672
        int $depth = 512,
3673
        int $options = 0
3674
    ) {
3675 43
        $json = self::filter($json);
3676
3677 43
        if (self::$SUPPORT['json'] === false) {
3678
            throw new \RuntimeException('ext-json: is not installed');
3679
        }
3680
3681
        /** @noinspection PhpComposerExtensionStubsInspection */
3682 43
        return \json_decode($json, $assoc, $depth, $options);
3683
    }
3684
3685
    /**
3686
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
3687
     * Returns the JSON representation of a value.
3688
     *
3689
     * @see http://php.net/manual/en/function.json-encode.php
3690
     *
3691
     * @param mixed $value   <p>
3692
     *                       The <i>value</i> being encoded. Can be any type except
3693
     *                       a resource.
3694
     *                       </p>
3695
     *                       <p>
3696
     *                       All string data must be UTF-8 encoded.
3697
     *                       </p>
3698
     *                       <p>PHP implements a superset of
3699
     *                       JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
3700
     *                       only supports these values when they are nested inside an array or an object.
3701
     *                       </p>
3702
     * @param int   $options [optional] <p>
3703
     *                       Bitmask consisting of <b>JSON_HEX_QUOT</b>,
3704
     *                       <b>JSON_HEX_TAG</b>,
3705
     *                       <b>JSON_HEX_AMP</b>,
3706
     *                       <b>JSON_HEX_APOS</b>,
3707
     *                       <b>JSON_NUMERIC_CHECK</b>,
3708
     *                       <b>JSON_PRETTY_PRINT</b>,
3709
     *                       <b>JSON_UNESCAPED_SLASHES</b>,
3710
     *                       <b>JSON_FORCE_OBJECT</b>,
3711
     *                       <b>JSON_UNESCAPED_UNICODE</b>. The behaviour of these
3712
     *                       constants is described on
3713
     *                       the JSON constants page.
3714
     *                       </p>
3715
     * @param int   $depth   [optional] <p>
3716
     *                       Set the maximum depth. Must be greater than zero.
3717
     *                       </p>
3718
     *
3719
     * @return false|string
3720
     *                      A JSON encoded <strong>string</strong> on success or<br>
3721
     *                      <strong>FALSE</strong> on failure
3722
     */
3723 5
    public static function json_encode($value, int $options = 0, int $depth = 512)
3724
    {
3725 5
        $value = self::filter($value);
3726
3727 5
        if (self::$SUPPORT['json'] === false) {
3728
            throw new \RuntimeException('ext-json: is not installed');
3729
        }
3730
3731
        /** @noinspection PhpComposerExtensionStubsInspection */
3732 5
        return \json_encode($value, $options, $depth);
3733
    }
3734
3735
    /**
3736
     * Checks whether JSON is available on the server.
3737
     *
3738
     * @return bool
3739
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3740
     */
3741
    public static function json_loaded(): bool
3742
    {
3743
        return \function_exists('json_decode');
3744
    }
3745
3746
    /**
3747
     * Makes string's first char lowercase.
3748
     *
3749
     * @param string      $str                           <p>The input string</p>
3750
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
3751
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
3752
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
3753
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
3754
     *
3755
     * @return string the resulting string
3756
     */
3757 46
    public static function lcfirst(
3758
        string $str,
3759
        string $encoding = 'UTF-8',
3760
        bool $clean_utf8 = false,
3761
        string $lang = null,
3762
        bool $try_to_keep_the_string_length = false
3763
    ): string {
3764 46
        if ($clean_utf8 === true) {
3765
            $str = self::clean($str);
3766
        }
3767
3768 46
        $use_mb_functions = ($lang === null && $try_to_keep_the_string_length === false);
3769
3770 46
        if ($encoding === 'UTF-8') {
3771 43
            $str_part_two = (string) \mb_substr($str, 1);
3772
3773 43
            if ($use_mb_functions === true) {
3774 43
                $str_part_one = \mb_strtolower(
3775 43
                    (string) \mb_substr($str, 0, 1)
3776
                );
3777
            } else {
3778
                $str_part_one = self::strtolower(
3779
                    (string) \mb_substr($str, 0, 1),
3780
                    $encoding,
3781
                    false,
3782
                    $lang,
3783 43
                    $try_to_keep_the_string_length
3784
                );
3785
            }
3786
        } else {
3787 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
3788
3789 3
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
3790
3791 3
            $str_part_one = self::strtolower(
3792 3
                (string) self::substr($str, 0, 1, $encoding),
3793 3
                $encoding,
3794 3
                false,
3795 3
                $lang,
3796 3
                $try_to_keep_the_string_length
3797
            );
3798
        }
3799
3800 46
        return $str_part_one . $str_part_two;
3801
    }
3802
3803
    /**
3804
     * alias for "UTF8::lcfirst()"
3805
     *
3806
     * @param string      $str
3807
     * @param string      $encoding
3808
     * @param bool        $clean_utf8
3809
     * @param string|null $lang
3810
     * @param bool        $try_to_keep_the_string_length
3811
     *
3812
     * @return string
3813
     *
3814
     * @see UTF8::lcfirst()
3815
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
3816
     */
3817 2
    public static function lcword(
3818
        string $str,
3819
        string $encoding = 'UTF-8',
3820
        bool $clean_utf8 = false,
3821
        string $lang = null,
3822
        bool $try_to_keep_the_string_length = false
3823
    ): string {
3824 2
        return self::lcfirst(
3825 2
            $str,
3826 2
            $encoding,
3827 2
            $clean_utf8,
3828 2
            $lang,
3829 2
            $try_to_keep_the_string_length
3830
        );
3831
    }
3832
3833
    /**
3834
     * Lowercase for all words in the string.
3835
     *
3836
     * @param string      $str                           <p>The input string.</p>
3837
     * @param string[]    $exceptions                    [optional] <p>Exclusion for some words.</p>
3838
     * @param string      $char_list                     [optional] <p>Additional chars that contains to words and do not start
3839
     *                                                   a new word.</p>
3840
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
3841
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
3842
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
3843
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
3844
     *
3845
     * @return string
3846
     */
3847 2
    public static function lcwords(
3848
        string $str,
3849
        array $exceptions = [],
3850
        string $char_list = '',
3851
        string $encoding = 'UTF-8',
3852
        bool $clean_utf8 = false,
3853
        string $lang = null,
3854
        bool $try_to_keep_the_string_length = false
3855
    ): string {
3856 2
        if (!$str) {
3857 2
            return '';
3858
        }
3859
3860 2
        $words = self::str_to_words($str, $char_list);
3861 2
        $use_exceptions = $exceptions !== [];
3862
3863 2
        foreach ($words as &$word) {
3864 2
            if (!$word) {
3865 2
                continue;
3866
            }
3867
3868
            if (
3869 2
                $use_exceptions === false
3870
                ||
3871 2
                !\in_array($word, $exceptions, true)
3872
            ) {
3873 2
                $word = self::lcfirst($word, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
3874
            }
3875
        }
3876
3877 2
        return \implode('', $words);
3878
    }
3879
3880
    /**
3881
     * alias for "UTF8::lcfirst()"
3882
     *
3883
     * @param string      $str
3884
     * @param string      $encoding
3885
     * @param bool        $clean_utf8
3886
     * @param string|null $lang
3887
     * @param bool        $try_to_keep_the_string_length
3888
     *
3889
     * @return string
3890
     *
3891
     * @see UTF8::lcfirst()
3892
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
3893
     */
3894 5
    public static function lowerCaseFirst(
3895
        string $str,
3896
        string $encoding = 'UTF-8',
3897
        bool $clean_utf8 = false,
3898
        string $lang = null,
3899
        bool $try_to_keep_the_string_length = false
3900
    ): string {
3901 5
        return self::lcfirst(
3902 5
            $str,
3903 5
            $encoding,
3904 5
            $clean_utf8,
3905 5
            $lang,
3906 5
            $try_to_keep_the_string_length
3907
        );
3908
    }
3909
3910
    /**
3911
     * Strip whitespace or other characters from the beginning of a UTF-8 string.
3912
     *
3913
     * @param string      $str   <p>The string to be trimmed</p>
3914
     * @param string|null $chars <p>Optional characters to be stripped</p>
3915
     *
3916
     * @return string the string with unwanted characters stripped from the left
3917
     */
3918 22
    public static function ltrim(string $str = '', string $chars = null): string
3919
    {
3920 22
        if ($str === '') {
3921 3
            return '';
3922
        }
3923
3924 21
        if (self::$SUPPORT['mbstring'] === true) {
3925 21
            if ($chars) {
3926
                /** @noinspection PregQuoteUsageInspection */
3927 10
                $chars = \preg_quote($chars);
3928 10
                $pattern = "^[${chars}]+";
3929
            } else {
3930 14
                $pattern = '^[\\s]+';
3931
            }
3932
3933
            /** @noinspection PhpComposerExtensionStubsInspection */
3934 21
            return (string) \mb_ereg_replace($pattern, '', $str);
3935
        }
3936
3937
        if ($chars) {
3938
            $chars = \preg_quote($chars, '/');
3939
            $pattern = "^[${chars}]+";
3940
        } else {
3941
            $pattern = '^[\\s]+';
3942
        }
3943
3944
        return self::regex_replace($str, $pattern, '', '', '/');
3945
    }
3946
3947
    /**
3948
     * Returns the UTF-8 character with the maximum code point in the given data.
3949
     *
3950
     * @param array<string>|string $arg <p>A UTF-8 encoded string or an array of such strings.</p>
3951
     *
3952
     * @return string|null the character with the highest code point than others, returns null on failure or empty input
3953
     */
3954
    public static function max($arg)
3955
    {
3956 2
        if (\is_array($arg) === true) {
3957 2
            $arg = \implode('', $arg);
3958
        }
3959
3960 2
        $codepoints = self::codepoints($arg, false);
3961 2
        if ($codepoints === []) {
3962 2
            return null;
3963
        }
3964
3965 2
        $codepoint_max = \max($codepoints);
3966
3967 2
        return self::chr($codepoint_max);
3968
    }
3969
3970
    /**
3971
     * Calculates and returns the maximum number of bytes taken by any
3972
     * UTF-8 encoded character in the given string.
3973
     *
3974
     * @param string $str <p>The original Unicode string.</p>
3975
     *
3976
     * @return int max byte lengths of the given chars
3977
     */
3978
    public static function max_chr_width(string $str): int
3979
    {
3980 2
        $bytes = self::chr_size_list($str);
3981 2
        if ($bytes !== []) {
3982 2
            return (int) \max($bytes);
3983
        }
3984
3985 2
        return 0;
3986
    }
3987
3988
    /**
3989
     * Checks whether mbstring is available on the server.
3990
     *
3991
     * @return bool
3992
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3993
     */
3994
    public static function mbstring_loaded(): bool
3995
    {
3996 28
        return \extension_loaded('mbstring');
3997
    }
3998
3999
    /**
4000
     * Returns the UTF-8 character with the minimum code point in the given data.
4001
     *
4002
     * @param mixed $arg <strong>A UTF-8 encoded string or an array of such strings.</strong>
4003
     *
4004
     * @return string|null the character with the lowest code point than others, returns null on failure or empty input
4005
     */
4006
    public static function min($arg)
4007
    {
4008 2
        if (\is_array($arg) === true) {
4009 2
            $arg = \implode('', $arg);
4010
        }
4011
4012 2
        $codepoints = self::codepoints($arg, false);
4013 2
        if ($codepoints === []) {
4014 2
            return null;
4015
        }
4016
4017 2
        $codepoint_min = \min($codepoints);
4018
4019 2
        return self::chr($codepoint_min);
4020
    }
4021
4022
    /**
4023
     * alias for "UTF8::normalize_encoding()"
4024
     *
4025
     * @param mixed $encoding
4026
     * @param mixed $fallback
4027
     *
4028
     * @return mixed
4029
     *
4030
     * @see UTF8::normalize_encoding()
4031
     * @deprecated <p>please use "UTF8::normalize_encoding()"</p>
4032
     */
4033
    public static function normalizeEncoding($encoding, $fallback = '')
4034
    {
4035 2
        return self::normalize_encoding($encoding, $fallback);
4036
    }
4037
4038
    /**
4039
     * Normalize the encoding-"name" input.
4040
     *
4041
     * @param mixed $encoding <p>e.g.: ISO, UTF8, WINDOWS-1251 etc.</p>
4042
     * @param mixed $fallback <p>e.g.: UTF-8</p>
4043
     *
4044
     * @return mixed e.g.: ISO-8859-1, UTF-8, WINDOWS-1251 etc.<br>Will return a empty string as fallback (by default)
4045
     */
4046
    public static function normalize_encoding($encoding, $fallback = '')
4047
    {
4048 331
        static $STATIC_NORMALIZE_ENCODING_CACHE = [];
4049
4050
        // init
4051 331
        $encoding = (string) $encoding;
4052
4053 331
        if (!$encoding) {
4054 285
            return $fallback;
4055
        }
4056
4057
        if (
4058 51
            $encoding === 'UTF-8'
4059
            ||
4060 51
            $encoding === 'UTF8'
4061
        ) {
4062 26
            return 'UTF-8';
4063
        }
4064
4065
        if (
4066 43
            $encoding === '8BIT'
4067
            ||
4068 43
            $encoding === 'BINARY'
4069
        ) {
4070
            return 'CP850';
4071
        }
4072
4073
        if (
4074 43
            $encoding === 'HTML'
4075
            ||
4076 43
            $encoding === 'HTML-ENTITIES'
4077
        ) {
4078 2
            return 'HTML-ENTITIES';
4079
        }
4080
4081
        if (
4082 43
            $encoding === '1' // only a fallback, for non "strict_types" usage ...
4083
            ||
4084 43
            $encoding === '0' // only a fallback, for non "strict_types" usage ...
4085
        ) {
4086 1
            return $fallback;
4087
        }
4088
4089 42
        if (isset($STATIC_NORMALIZE_ENCODING_CACHE[$encoding])) {
4090 40
            return $STATIC_NORMALIZE_ENCODING_CACHE[$encoding];
4091
        }
4092
4093 6
        if (self::$ENCODINGS === null) {
4094 1
            self::$ENCODINGS = self::getData('encodings');
4095
        }
4096
4097 6
        if (\in_array($encoding, self::$ENCODINGS, true)) {
4098 4
            $STATIC_NORMALIZE_ENCODING_CACHE[$encoding] = $encoding;
4099
4100 4
            return $encoding;
4101
        }
4102
4103 5
        $encoding_original = $encoding;
4104 5
        $encoding = \strtoupper($encoding);
4105 5
        $encoding_upper_helper = (string) \preg_replace('/[^a-zA-Z0-9]/u', '', $encoding);
4106
4107
        $equivalences = [
4108 5
            'ISO8859'     => 'ISO-8859-1',
4109
            'ISO88591'    => 'ISO-8859-1',
4110
            'ISO'         => 'ISO-8859-1',
4111
            'LATIN'       => 'ISO-8859-1',
4112
            'LATIN1'      => 'ISO-8859-1', // Western European
4113
            'ISO88592'    => 'ISO-8859-2',
4114
            'LATIN2'      => 'ISO-8859-2', // Central European
4115
            'ISO88593'    => 'ISO-8859-3',
4116
            'LATIN3'      => 'ISO-8859-3', // Southern European
4117
            'ISO88594'    => 'ISO-8859-4',
4118
            'LATIN4'      => 'ISO-8859-4', // Northern European
4119
            'ISO88595'    => 'ISO-8859-5',
4120
            'ISO88596'    => 'ISO-8859-6', // Greek
4121
            'ISO88597'    => 'ISO-8859-7',
4122
            'ISO88598'    => 'ISO-8859-8', // Hebrew
4123
            'ISO88599'    => 'ISO-8859-9',
4124
            'LATIN5'      => 'ISO-8859-9', // Turkish
4125
            'ISO885911'   => 'ISO-8859-11',
4126
            'TIS620'      => 'ISO-8859-11', // Thai
4127
            'ISO885910'   => 'ISO-8859-10',
4128
            'LATIN6'      => 'ISO-8859-10', // Nordic
4129
            'ISO885913'   => 'ISO-8859-13',
4130
            'LATIN7'      => 'ISO-8859-13', // Baltic
4131
            'ISO885914'   => 'ISO-8859-14',
4132
            'LATIN8'      => 'ISO-8859-14', // Celtic
4133
            'ISO885915'   => 'ISO-8859-15',
4134
            'LATIN9'      => 'ISO-8859-15', // Western European (with some extra chars e.g. €)
4135
            'ISO885916'   => 'ISO-8859-16',
4136
            'LATIN10'     => 'ISO-8859-16', // Southeast European
4137
            'CP1250'      => 'WINDOWS-1250',
4138
            'WIN1250'     => 'WINDOWS-1250',
4139
            'WINDOWS1250' => 'WINDOWS-1250',
4140
            'CP1251'      => 'WINDOWS-1251',
4141
            'WIN1251'     => 'WINDOWS-1251',
4142
            'WINDOWS1251' => 'WINDOWS-1251',
4143
            'CP1252'      => 'WINDOWS-1252',
4144
            'WIN1252'     => 'WINDOWS-1252',
4145
            'WINDOWS1252' => 'WINDOWS-1252',
4146
            'CP1253'      => 'WINDOWS-1253',
4147
            'WIN1253'     => 'WINDOWS-1253',
4148
            'WINDOWS1253' => 'WINDOWS-1253',
4149
            'CP1254'      => 'WINDOWS-1254',
4150
            'WIN1254'     => 'WINDOWS-1254',
4151
            'WINDOWS1254' => 'WINDOWS-1254',
4152
            'CP1255'      => 'WINDOWS-1255',
4153
            'WIN1255'     => 'WINDOWS-1255',
4154
            'WINDOWS1255' => 'WINDOWS-1255',
4155
            'CP1256'      => 'WINDOWS-1256',
4156
            'WIN1256'     => 'WINDOWS-1256',
4157
            'WINDOWS1256' => 'WINDOWS-1256',
4158
            'CP1257'      => 'WINDOWS-1257',
4159
            'WIN1257'     => 'WINDOWS-1257',
4160
            'WINDOWS1257' => 'WINDOWS-1257',
4161
            'CP1258'      => 'WINDOWS-1258',
4162
            'WIN1258'     => 'WINDOWS-1258',
4163
            'WINDOWS1258' => 'WINDOWS-1258',
4164
            'UTF16'       => 'UTF-16',
4165
            'UTF32'       => 'UTF-32',
4166
            'UTF8'        => 'UTF-8',
4167
            'UTF'         => 'UTF-8',
4168
            'UTF7'        => 'UTF-7',
4169
            '8BIT'        => 'CP850',
4170
            'BINARY'      => 'CP850',
4171
        ];
4172
4173 5
        if (!empty($equivalences[$encoding_upper_helper])) {
4174 4
            $encoding = $equivalences[$encoding_upper_helper];
4175
        }
4176
4177 5
        $STATIC_NORMALIZE_ENCODING_CACHE[$encoding_original] = $encoding;
4178
4179 5
        return $encoding;
4180
    }
4181
4182
    /**
4183
     * Standardize line ending to unix-like.
4184
     *
4185
     * @param string $str
4186
     *
4187
     * @return string
4188
     */
4189
    public static function normalize_line_ending(string $str): string
4190
    {
4191 5
        return \str_replace(["\r\n", "\r"], "\n", $str);
4192
    }
4193
4194
    /**
4195
     * Normalize some MS Word special characters.
4196
     *
4197
     * @param string $str <p>The string to be normalized.</p>
4198
     *
4199
     * @return string
4200
     */
4201
    public static function normalize_msword(string $str): string
4202
    {
4203 10
        return ASCII::normalize_msword($str);
4204
    }
4205
4206
    /**
4207
     * Normalize the whitespace.
4208
     *
4209
     * @param string $str                        <p>The string to be normalized.</p>
4210
     * @param bool   $keep_non_breaking_space    [optional] <p>Set to true, to keep non-breaking-spaces.</p>
4211
     * @param bool   $keep_bidi_unicode_controls [optional] <p>Set to true, to keep non-printable (for the web)
4212
     *                                           bidirectional text chars.</p>
4213
     *
4214
     * @return string
4215
     */
4216
    public static function normalize_whitespace(
4217
        string $str,
4218
        bool $keep_non_breaking_space = false,
4219
        bool $keep_bidi_unicode_controls = false
4220
    ): string {
4221 61
        return ASCII::normalize_whitespace(
4222 61
            $str,
4223 61
            $keep_non_breaking_space,
4224 61
            $keep_bidi_unicode_controls
4225
        );
4226
    }
4227
4228
    /**
4229
     * Calculates Unicode code point of the given UTF-8 encoded character.
4230
     *
4231
     * INFO: opposite to UTF8::chr()
4232
     *
4233
     * @param string $chr      <p>The character of which to calculate code point.<p/>
4234
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
4235
     *
4236
     * @return int
4237
     *             Unicode code point of the given character,<br>
4238
     *             0 on invalid UTF-8 byte sequence
4239
     */
4240
    public static function ord($chr, string $encoding = 'UTF-8'): int
4241
    {
4242 30
        static $CHAR_CACHE = [];
4243
4244
        // init
4245 30
        $chr = (string) $chr;
4246
4247 30
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
4248 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4249
        }
4250
4251 30
        $cache_key = $chr . $encoding;
4252 30
        if (isset($CHAR_CACHE[$cache_key]) === true) {
4253 30
            return $CHAR_CACHE[$cache_key];
4254
        }
4255
4256
        // check again, if it's still not UTF-8
4257 12
        if ($encoding !== 'UTF-8') {
4258 3
            $chr = self::encode($encoding, $chr);
4259
        }
4260
4261 12
        if (self::$ORD === null) {
4262
            self::$ORD = self::getData('ord');
4263
        }
4264
4265 12
        if (isset(self::$ORD[$chr])) {
4266 12
            return $CHAR_CACHE[$cache_key] = self::$ORD[$chr];
4267
        }
4268
4269
        //
4270
        // fallback via "IntlChar"
4271
        //
4272
4273 6
        if (self::$SUPPORT['intlChar'] === true) {
4274
            /** @noinspection PhpComposerExtensionStubsInspection */
4275 5
            $code = \IntlChar::ord($chr);
4276 5
            if ($code) {
4277 5
                return $CHAR_CACHE[$cache_key] = $code;
4278
            }
4279
        }
4280
4281
        //
4282
        // fallback via vanilla php
4283
        //
4284
4285
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
4286 1
        $chr = \unpack('C*', (string) \substr($chr, 0, 4));
4287
        /** @noinspection OffsetOperationsInspection */
4288 1
        $code = $chr ? $chr[1] : 0;
4289
4290
        /** @noinspection OffsetOperationsInspection */
4291 1
        if ($code >= 0xF0 && isset($chr[4])) {
4292
            /** @noinspection UnnecessaryCastingInspection */
4293
            /** @noinspection OffsetOperationsInspection */
4294
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xF0) << 18) + (($chr[2] - 0x80) << 12) + (($chr[3] - 0x80) << 6) + $chr[4] - 0x80);
4295
        }
4296
4297
        /** @noinspection OffsetOperationsInspection */
4298 1
        if ($code >= 0xE0 && isset($chr[3])) {
4299
            /** @noinspection UnnecessaryCastingInspection */
4300
            /** @noinspection OffsetOperationsInspection */
4301 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xE0) << 12) + (($chr[2] - 0x80) << 6) + $chr[3] - 0x80);
4302
        }
4303
4304
        /** @noinspection OffsetOperationsInspection */
4305 1
        if ($code >= 0xC0 && isset($chr[2])) {
4306
            /** @noinspection UnnecessaryCastingInspection */
4307
            /** @noinspection OffsetOperationsInspection */
4308 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xC0) << 6) + $chr[2] - 0x80);
4309
        }
4310
4311
        return $CHAR_CACHE[$cache_key] = $code;
4312
    }
4313
4314
    /**
4315
     * Parses the string into an array (into the the second parameter).
4316
     *
4317
     * WARNING: Unlike "parse_str()", this method does not (re-)place variables in the current scope,
4318
     *          if the second parameter is not set!
4319
     *
4320
     * @see http://php.net/manual/en/function.parse-str.php
4321
     *
4322
     * @param string $str        <p>The input string.</p>
4323
     * @param array  $result     <p>The result will be returned into this reference parameter.</p>
4324
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
4325
     *
4326
     * @return bool
4327
     *              Will return <strong>false</strong> if php can't parse the string and we haven't any $result
4328
     */
4329
    public static function parse_str(string $str, &$result, bool $clean_utf8 = false): bool
4330
    {
4331 2
        if ($clean_utf8 === true) {
4332 2
            $str = self::clean($str);
4333
        }
4334
4335 2
        if (self::$SUPPORT['mbstring'] === true) {
4336 2
            $return = \mb_parse_str($str, $result);
4337
4338 2
            return $return !== false && $result !== [];
4339
        }
4340
4341
        /** @noinspection PhpVoidFunctionResultUsedInspection */
4342
        \parse_str($str, $result);
4343
4344
        return $result !== [];
4345
    }
4346
4347
    /**
4348
     * Checks if \u modifier is available that enables Unicode support in PCRE.
4349
     *
4350
     * @return bool
4351
     *              <strong>true</strong> if support is available,<br>
4352
     *              <strong>false</strong> otherwise
4353
     */
4354
    public static function pcre_utf8_support(): bool
4355
    {
4356
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
4357 102
        return (bool) @\preg_match('//u', '');
4358
    }
4359
4360
    /**
4361
     * Create an array containing a range of UTF-8 characters.
4362
     *
4363
     * @param mixed     $var1      <p>Numeric or hexadecimal code points, or a UTF-8 character to start from.</p>
4364
     * @param mixed     $var2      <p>Numeric or hexadecimal code points, or a UTF-8 character to end at.</p>
4365
     * @param bool      $use_ctype <p>use ctype to detect numeric and hexadecimal, otherwise we will use a simple "is_numeric"</p>
4366
     * @param string    $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
4367
     * @param float|int $step      [optional] <p>
4368
     *                             If a step value is given, it will be used as the
4369
     *                             increment between elements in the sequence. step
4370
     *                             should be given as a positive number. If not specified,
4371
     *                             step will default to 1.
4372
     *                             </p>
4373
     *
4374
     * @return string[]
4375
     */
4376
    public static function range(
4377
        $var1,
4378
        $var2,
4379
        bool $use_ctype = true,
4380
        string $encoding = 'UTF-8',
4381
        $step = 1
4382
    ): array {
4383 2
        if (!$var1 || !$var2) {
4384 2
            return [];
4385
        }
4386
4387 2
        if ($step !== 1) {
4388 1
            if (!\is_numeric($step)) {
0 ignored issues
show
introduced by
The condition is_numeric($step) is always true.
Loading history...
4389
                throw new \InvalidArgumentException('$step need to be a number, type given: ' . \gettype($step));
4390
            }
4391
4392 1
            if ($step <= 0) {
4393
                throw new \InvalidArgumentException('$step need to be a positive number, given: ' . $step);
4394
            }
4395
        }
4396
4397 2
        if ($use_ctype && self::$SUPPORT['ctype'] === false) {
4398
            throw new \RuntimeException('ext-ctype: is not installed');
4399
        }
4400
4401 2
        $is_digit = false;
4402 2
        $is_xdigit = false;
4403
4404
        /** @noinspection PhpComposerExtensionStubsInspection */
4405 2
        if ($use_ctype && \ctype_digit((string) $var1) && \ctype_digit((string) $var2)) {
4406 2
            $is_digit = true;
4407 2
            $start = (int) $var1;
4408 2
        } /** @noinspection PhpComposerExtensionStubsInspection */ elseif ($use_ctype && \ctype_xdigit($var1) && \ctype_xdigit($var2)) {
4409
            $is_xdigit = true;
4410
            $start = (int) self::hex_to_int($var1);
4411 2
        } elseif (!$use_ctype && \is_numeric($var1)) {
4412 1
            $start = (int) $var1;
4413
        } else {
4414 2
            $start = self::ord($var1);
4415
        }
4416
4417 2
        if (!$start) {
4418
            return [];
4419
        }
4420
4421 2
        if ($is_digit) {
4422 2
            $end = (int) $var2;
4423 2
        } elseif ($is_xdigit) {
4424
            $end = (int) self::hex_to_int($var2);
4425 2
        } elseif (!$use_ctype && \is_numeric($var2)) {
4426 1
            $end = (int) $var2;
4427
        } else {
4428 2
            $end = self::ord($var2);
4429
        }
4430
4431 2
        if (!$end) {
4432
            return [];
4433
        }
4434
4435 2
        $array = [];
4436 2
        foreach (\range($start, $end, $step) as $i) {
4437 2
            $array[] = (string) self::chr((int) $i, $encoding);
4438
        }
4439
4440 2
        return $array;
4441
    }
4442
4443
    /**
4444
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
4445
     *
4446
     * e.g:
4447
     * 'test+test'                     => 'test+test'
4448
     * 'D&#252;sseldorf'               => 'Düsseldorf'
4449
     * 'D%FCsseldorf'                  => 'Düsseldorf'
4450
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
4451
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
4452
     * 'Düsseldorf'                   => 'Düsseldorf'
4453
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
4454
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
4455
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
4456
     *
4457
     * @param string $str          <p>The input string.</p>
4458
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
4459
     *
4460
     * @return string
4461
     */
4462
    public static function rawurldecode(string $str, bool $multi_decode = true): string
4463
    {
4464 6
        if ($str === '') {
4465 4
            return '';
4466
        }
4467
4468
        if (
4469 6
            \strpos($str, '&') === false
4470
            &&
4471 6
            \strpos($str, '%') === false
4472
            &&
4473 6
            \strpos($str, '+') === false
4474
            &&
4475 6
            \strpos($str, '\u') === false
4476
        ) {
4477 4
            return self::fix_simple_utf8($str);
4478
        }
4479
4480 6
        $str = self::urldecode_unicode_helper($str);
4481
4482
        do {
4483 6
            $str_compare = $str;
4484
4485
            /**
4486
             * @psalm-suppress PossiblyInvalidArgument
4487
             */
4488 6
            $str = self::fix_simple_utf8(
4489 6
                \rawurldecode(
4490 6
                    self::html_entity_decode(
4491 6
                        self::to_utf8($str),
4492 6
                        \ENT_QUOTES | \ENT_HTML5
4493
                    )
4494
                )
4495
            );
4496 6
        } while ($multi_decode === true && $str_compare !== $str);
4497
4498 6
        return $str;
4499
    }
4500
4501
    /**
4502
     * Replaces all occurrences of $pattern in $str by $replacement.
4503
     *
4504
     * @param string $str         <p>The input string.</p>
4505
     * @param string $pattern     <p>The regular expression pattern.</p>
4506
     * @param string $replacement <p>The string to replace with.</p>
4507
     * @param string $options     [optional] <p>Matching conditions to be used.</p>
4508
     * @param string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
4509
     *
4510
     * @return string
4511
     */
4512
    public static function regex_replace(
4513
        string $str,
4514
        string $pattern,
4515
        string $replacement,
4516
        string $options = '',
4517
        string $delimiter = '/'
4518
    ): string {
4519 18
        if ($options === 'msr') {
4520 9
            $options = 'ms';
4521
        }
4522
4523
        // fallback
4524 18
        if (!$delimiter) {
4525
            $delimiter = '/';
4526
        }
4527
4528 18
        return (string) \preg_replace(
4529 18
            $delimiter . $pattern . $delimiter . 'u' . $options,
4530 18
            $replacement,
4531 18
            $str
4532
        );
4533
    }
4534
4535
    /**
4536
     * alias for "UTF8::remove_bom()"
4537
     *
4538
     * @param string $str
4539
     *
4540
     * @return string
4541
     *
4542
     * @see UTF8::remove_bom()
4543
     * @deprecated <p>please use "UTF8::remove_bom()"</p>
4544
     */
4545
    public static function removeBOM(string $str): string
4546
    {
4547
        return self::remove_bom($str);
4548
    }
4549
4550
    /**
4551
     * Remove the BOM from UTF-8 / UTF-16 / UTF-32 strings.
4552
     *
4553
     * @param string $str <p>The input string.</p>
4554
     *
4555
     * @return string string without UTF-BOM
4556
     */
4557
    public static function remove_bom(string $str): string
4558
    {
4559 55
        if ($str === '') {
4560 9
            return '';
4561
        }
4562
4563 55
        $str_length = \strlen($str);
4564 55
        foreach (self::$BOM as $bom_string => $bom_byte_length) {
4565 55
            if (\strpos($str, $bom_string, 0) === 0) {
4566
                /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
4567 11
                $str_tmp = \substr($str, $bom_byte_length, $str_length);
4568 11
                if ($str_tmp === false) {
4569
                    return '';
4570
                }
4571
4572 11
                $str_length -= (int) $bom_byte_length;
4573
4574 55
                $str = (string) $str_tmp;
4575
            }
4576
        }
4577
4578 55
        return $str;
4579
    }
4580
4581
    /**
4582
     * Removes duplicate occurrences of a string in another string.
4583
     *
4584
     * @param string          $str  <p>The base string.</p>
4585
     * @param string|string[] $what <p>String to search for in the base string.</p>
4586
     *
4587
     * @return string the result string with removed duplicates
4588
     */
4589
    public static function remove_duplicates(string $str, $what = ' '): string
4590
    {
4591 2
        if (\is_string($what) === true) {
4592 2
            $what = [$what];
4593
        }
4594
4595 2
        if (\is_array($what) === true) {
0 ignored issues
show
introduced by
The condition is_array($what) === true is always true.
Loading history...
4596
            /** @noinspection ForeachSourceInspection */
4597 2
            foreach ($what as $item) {
4598 2
                $str = (string) \preg_replace('/(' . \preg_quote($item, '/') . ')+/u', $item, $str);
4599
            }
4600
        }
4601
4602 2
        return $str;
4603
    }
4604
4605
    /**
4606
     * Remove html via "strip_tags()" from the string.
4607
     *
4608
     * @param string $str
4609
     * @param string $allowable_tags [optional] <p>You can use the optional second parameter to specify tags which should
4610
     *                               not be stripped. Default: null
4611
     *                               </p>
4612
     *
4613
     * @return string
4614
     */
4615
    public static function remove_html(string $str, string $allowable_tags = ''): string
4616
    {
4617 6
        return \strip_tags($str, $allowable_tags);
4618
    }
4619
4620
    /**
4621
     * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
4622
     *
4623
     * @param string $str
4624
     * @param string $replacement [optional] <p>Default is a empty string.</p>
4625
     *
4626
     * @return string
4627
     */
4628
    public static function remove_html_breaks(string $str, string $replacement = ''): string
4629
    {
4630 6
        return (string) \preg_replace("#/\r\n|\r|\n|<br.*/?>#isU", $replacement, $str);
4631
    }
4632
4633
    /**
4634
     * Remove invisible characters from a string.
4635
     *
4636
     * e.g.: This prevents sandwiching null characters between ascii characters, like Java\0script.
4637
     *
4638
     * copy&past from https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Common.php
4639
     *
4640
     * @param string $str
4641
     * @param bool   $url_encoded
4642
     * @param string $replacement
4643
     *
4644
     * @return string
4645
     */
4646
    public static function remove_invisible_characters(
4647
        string $str,
4648
        bool $url_encoded = true,
4649
        string $replacement = ''
4650
    ): string {
4651 89
        return ASCII::remove_invisible_characters(
4652 89
            $str,
4653 89
            $url_encoded,
4654 89
            $replacement
4655
        );
4656
    }
4657
4658
    /**
4659
     * Returns a new string with the prefix $substring removed, if present.
4660
     *
4661
     * @param string $str
4662
     * @param string $substring <p>The prefix to remove.</p>
4663
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
4664
     *
4665
     * @return string string without the prefix $substring
4666
     */
4667
    public static function remove_left(
4668
        string $str,
4669
        string $substring,
4670
        string $encoding = 'UTF-8'
4671
    ): string {
4672 12
        if ($substring && \strpos($str, $substring) === 0) {
4673 6
            if ($encoding === 'UTF-8') {
4674 4
                return (string) \mb_substr(
4675 4
                    $str,
4676 4
                    (int) \mb_strlen($substring)
4677
                );
4678
            }
4679
4680 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4681
4682 2
            return (string) self::substr(
4683 2
                $str,
4684 2
                (int) self::strlen($substring, $encoding),
4685 2
                null,
4686 2
                $encoding
4687
            );
4688
        }
4689
4690 6
        return $str;
4691
    }
4692
4693
    /**
4694
     * Returns a new string with the suffix $substring removed, if present.
4695
     *
4696
     * @param string $str
4697
     * @param string $substring <p>The suffix to remove.</p>
4698
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
4699
     *
4700
     * @return string string having a $str without the suffix $substring
4701
     */
4702
    public static function remove_right(
4703
        string $str,
4704
        string $substring,
4705
        string $encoding = 'UTF-8'
4706
    ): string {
4707 12
        if ($substring && \substr($str, -\strlen($substring)) === $substring) {
4708 6
            if ($encoding === 'UTF-8') {
4709 4
                return (string) \mb_substr(
4710 4
                    $str,
4711 4
                    0,
4712 4
                    (int) \mb_strlen($str) - (int) \mb_strlen($substring)
4713
                );
4714
            }
4715
4716 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4717
4718 2
            return (string) self::substr(
4719 2
                $str,
4720 2
                0,
4721 2
                (int) self::strlen($str, $encoding) - (int) self::strlen($substring, $encoding),
4722 2
                $encoding
4723
            );
4724
        }
4725
4726 6
        return $str;
4727
    }
4728
4729
    /**
4730
     * Replaces all occurrences of $search in $str by $replacement.
4731
     *
4732
     * @param string $str            <p>The input string.</p>
4733
     * @param string $search         <p>The needle to search for.</p>
4734
     * @param string $replacement    <p>The string to replace with.</p>
4735
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
4736
     *
4737
     * @return string string after the replacements
4738
     */
4739
    public static function replace(
4740
        string $str,
4741
        string $search,
4742
        string $replacement,
4743
        bool $case_sensitive = true
4744
    ): string {
4745 29
        if ($case_sensitive) {
4746 22
            return \str_replace($search, $replacement, $str);
4747
        }
4748
4749 7
        return self::str_ireplace($search, $replacement, $str);
4750
    }
4751
4752
    /**
4753
     * Replaces all occurrences of $search in $str by $replacement.
4754
     *
4755
     * @param string       $str            <p>The input string.</p>
4756
     * @param array        $search         <p>The elements to search for.</p>
4757
     * @param array|string $replacement    <p>The string to replace with.</p>
4758
     * @param bool         $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
4759
     *
4760
     * @return string string after the replacements
4761
     */
4762
    public static function replace_all(
4763
        string $str,
4764
        array $search,
4765
        $replacement,
4766
        bool $case_sensitive = true
4767
    ): string {
4768 30
        if ($case_sensitive) {
4769 23
            return \str_replace($search, $replacement, $str);
4770
        }
4771
4772 7
        return self::str_ireplace($search, $replacement, $str);
4773
    }
4774
4775
    /**
4776
     * Replace the diamond question mark (�) and invalid-UTF8 chars with the replacement.
4777
     *
4778
     * @param string $str                        <p>The input string</p>
4779
     * @param string $replacement_char           <p>The replacement character.</p>
4780
     * @param bool   $process_invalid_utf8_chars <p>Convert invalid UTF-8 chars </p>
4781
     *
4782
     * @return string
4783
     */
4784
    public static function replace_diamond_question_mark(
4785
        string $str,
4786
        string $replacement_char = '',
4787
        bool $process_invalid_utf8_chars = true
4788
    ): string {
4789 35
        if ($str === '') {
4790 9
            return '';
4791
        }
4792
4793 35
        if ($process_invalid_utf8_chars === true) {
4794 35
            $replacement_char_helper = $replacement_char;
4795 35
            if ($replacement_char === '') {
4796 35
                $replacement_char_helper = 'none';
4797
            }
4798
4799 35
            if (self::$SUPPORT['mbstring'] === false) {
4800
                // if there is no native support for "mbstring",
4801
                // then we need to clean the string before ...
4802
                $str = self::clean($str);
4803
            }
4804
4805 35
            $save = \mb_substitute_character();
4806 35
            \mb_substitute_character($replacement_char_helper);
4807
            // the polyfill maybe return false, so cast to string
4808 35
            $str = (string) \mb_convert_encoding($str, 'UTF-8', 'UTF-8');
4809 35
            \mb_substitute_character($save);
4810
        }
4811
4812 35
        return \str_replace(
4813
            [
4814 35
                "\xEF\xBF\xBD",
4815
                '�',
4816
            ],
4817
            [
4818 35
                $replacement_char,
4819 35
                $replacement_char,
4820
            ],
4821 35
            $str
4822
        );
4823
    }
4824
4825
    /**
4826
     * Strip whitespace or other characters from the end of a UTF-8 string.
4827
     *
4828
     * @param string      $str   <p>The string to be trimmed.</p>
4829
     * @param string|null $chars <p>Optional characters to be stripped.</p>
4830
     *
4831
     * @return string the string with unwanted characters stripped from the right
4832
     */
4833
    public static function rtrim(string $str = '', string $chars = null): string
4834
    {
4835 20
        if ($str === '') {
4836 3
            return '';
4837
        }
4838
4839 19
        if (self::$SUPPORT['mbstring'] === true) {
4840 19
            if ($chars) {
4841
                /** @noinspection PregQuoteUsageInspection */
4842 8
                $chars = \preg_quote($chars);
4843 8
                $pattern = "[${chars}]+$";
4844
            } else {
4845 14
                $pattern = '[\\s]+$';
4846
            }
4847
4848
            /** @noinspection PhpComposerExtensionStubsInspection */
4849 19
            return (string) \mb_ereg_replace($pattern, '', $str);
4850
        }
4851
4852
        if ($chars) {
4853
            $chars = \preg_quote($chars, '/');
4854
            $pattern = "[${chars}]+$";
4855
        } else {
4856
            $pattern = '[\\s]+$';
4857
        }
4858
4859
        return self::regex_replace($str, $pattern, '', '', '/');
4860
    }
4861
4862
    /**
4863
     * WARNING: Print native UTF-8 support (libs), e.g. for debugging.
4864
     *
4865
     * @psalm-suppress MissingReturnType
4866
     */
4867
    public static function showSupport()
4868
    {
4869 2
        echo '<pre>';
4870 2
        foreach (self::$SUPPORT as $key => &$value) {
4871 2
            echo $key . ' - ' . \print_r($value, true) . "\n<br>";
4872
        }
4873 2
        unset($value);
4874 2
        echo '</pre>';
4875 2
    }
4876
4877
    /**
4878
     * Converts a UTF-8 character to HTML Numbered Entity like "&#123;".
4879
     *
4880
     * @param string $char             <p>The Unicode character to be encoded as numbered entity.</p>
4881
     * @param bool   $keep_ascii_chars <p>Set to <strong>true</strong> to keep ASCII chars.</>
4882
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
4883
     *
4884
     * @return string the HTML numbered entity
4885
     */
4886
    public static function single_chr_html_encode(
4887
        string $char,
4888
        bool $keep_ascii_chars = false,
4889
        string $encoding = 'UTF-8'
4890
    ): string {
4891 2
        if ($char === '') {
4892 2
            return '';
4893
        }
4894
4895
        if (
4896 2
            $keep_ascii_chars === true
4897
            &&
4898 2
            ASCII::is_ascii($char) === true
4899
        ) {
4900 2
            return $char;
4901
        }
4902
4903 2
        return '&#' . self::ord($char, $encoding) . ';';
4904
    }
4905
4906
    /**
4907
     * @param string $str
4908
     * @param int    $tab_length
4909
     *
4910
     * @return string
4911
     */
4912
    public static function spaces_to_tabs(string $str, int $tab_length = 4): string
4913
    {
4914 5
        if ($tab_length === 4) {
4915 3
            $tab = '    ';
4916 2
        } elseif ($tab_length === 2) {
4917 1
            $tab = '  ';
4918
        } else {
4919 1
            $tab = \str_repeat(' ', $tab_length);
4920
        }
4921
4922 5
        return \str_replace($tab, "\t", $str);
4923
    }
4924
4925
    /**
4926
     * alias for "UTF8::str_split()"
4927
     *
4928
     * @param string|string[] $str
4929
     * @param int             $length
4930
     * @param bool            $clean_utf8
4931
     *
4932
     * @return string[]
4933
     *
4934
     * @see UTF8::str_split()
4935
     * @deprecated <p>please use "UTF8::str_split()"</p>
4936
     */
4937
    public static function split(
4938
        $str,
4939
        int $length = 1,
4940
        bool $clean_utf8 = false
4941
    ): array {
4942 9
        return self::str_split($str, $length, $clean_utf8);
4943
    }
4944
4945
    /**
4946
     * alias for "UTF8::str_starts_with()"
4947
     *
4948
     * @param string $haystack
4949
     * @param string $needle
4950
     *
4951
     * @return bool
4952
     *
4953
     * @see UTF8::str_starts_with()
4954
     * @deprecated <p>please use "UTF8::str_starts_with()"</p>
4955
     */
4956
    public static function str_begins(string $haystack, string $needle): bool
4957
    {
4958
        return self::str_starts_with($haystack, $needle);
4959
    }
4960
4961
    /**
4962
     * Returns a camelCase version of the string. Trims surrounding spaces,
4963
     * capitalizes letters following digits, spaces, dashes and underscores,
4964
     * and removes spaces, dashes, as well as underscores.
4965
     *
4966
     * @param string      $str                           <p>The input string.</p>
4967
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
4968
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4969
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
4970
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
4971
     *
4972
     * @return string
4973
     */
4974
    public static function str_camelize(
4975
        string $str,
4976
        string $encoding = 'UTF-8',
4977
        bool $clean_utf8 = false,
4978
        string $lang = null,
4979
        bool $try_to_keep_the_string_length = false
4980
    ): string {
4981 32
        if ($clean_utf8 === true) {
4982
            $str = self::clean($str);
4983
        }
4984
4985 32
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
4986 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4987
        }
4988
4989 32
        $str = self::lcfirst(
4990 32
            \trim($str),
4991 32
            $encoding,
4992 32
            false,
4993 32
            $lang,
4994 32
            $try_to_keep_the_string_length
4995
        );
4996 32
        $str = (string) \preg_replace('/^[-_]+/', '', $str);
4997
4998 32
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
4999
5000 32
        $str = (string) \preg_replace_callback(
5001 32
            '/[-_\\s]+(.)?/u',
5002
            /**
5003
             * @param array $match
5004
             *
5005
             * @return string
5006
             */
5007
            static function (array $match) use ($use_mb_functions, $encoding, $lang, $try_to_keep_the_string_length): string {
5008 27
                if (isset($match[1])) {
5009 27
                    if ($use_mb_functions === true) {
5010 27
                        if ($encoding === 'UTF-8') {
5011 27
                            return \mb_strtoupper($match[1]);
5012
                        }
5013
5014
                        return \mb_strtoupper($match[1], $encoding);
5015
                    }
5016
5017
                    return self::strtoupper($match[1], $encoding, false, $lang, $try_to_keep_the_string_length);
5018
                }
5019
5020 1
                return '';
5021 32
            },
5022 32
            $str
5023
        );
5024
5025 32
        return (string) \preg_replace_callback(
5026 32
            '/[\\p{N}]+(.)?/u',
5027
            /**
5028
             * @param array $match
5029
             *
5030
             * @return string
5031
             */
5032
            static function (array $match) use ($use_mb_functions, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length): string {
5033 6
                if ($use_mb_functions === true) {
5034 6
                    if ($encoding === 'UTF-8') {
5035 6
                        return \mb_strtoupper($match[0]);
5036
                    }
5037
5038
                    return \mb_strtoupper($match[0], $encoding);
5039
                }
5040
5041
                return self::strtoupper($match[0], $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5042 32
            },
5043 32
            $str
5044
        );
5045
    }
5046
5047
    /**
5048
     * Returns the string with the first letter of each word capitalized,
5049
     * except for when the word is a name which shouldn't be capitalized.
5050
     *
5051
     * @param string $str
5052
     *
5053
     * @return string string with $str capitalized
5054
     */
5055
    public static function str_capitalize_name(string $str): string
5056
    {
5057 1
        return self::str_capitalize_name_helper(
5058 1
            self::str_capitalize_name_helper(
5059 1
                self::collapse_whitespace($str),
5060 1
                ' '
5061
            ),
5062 1
            '-'
5063
        );
5064
    }
5065
5066
    /**
5067
     * Returns true if the string contains $needle, false otherwise. By default
5068
     * the comparison is case-sensitive, but can be made insensitive by setting
5069
     * $case_sensitive to false.
5070
     *
5071
     * @param string $haystack       <p>The input string.</p>
5072
     * @param string $needle         <p>Substring to look for.</p>
5073
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5074
     *
5075
     * @return bool whether or not $haystack contains $needle
5076
     */
5077
    public static function str_contains(
5078
        string $haystack,
5079
        string $needle,
5080
        bool $case_sensitive = true
5081
    ): bool {
5082 21
        if ($case_sensitive) {
5083 11
            return \strpos($haystack, $needle) !== false;
5084
        }
5085
5086 10
        return \mb_stripos($haystack, $needle) !== false;
5087
    }
5088
5089
    /**
5090
     * Returns true if the string contains all $needles, false otherwise. By
5091
     * default the comparison is case-sensitive, but can be made insensitive by
5092
     * setting $case_sensitive to false.
5093
     *
5094
     * @param string $haystack       <p>The input string.</p>
5095
     * @param array  $needles        <p>SubStrings to look for.</p>
5096
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5097
     *
5098
     * @return bool whether or not $haystack contains $needle
5099
     */
5100
    public static function str_contains_all(
5101
        string $haystack,
5102
        array $needles,
5103
        bool $case_sensitive = true
5104
    ): bool {
5105 44
        if ($haystack === '' || $needles === []) {
5106 1
            return false;
5107
        }
5108
5109
        /** @noinspection LoopWhichDoesNotLoopInspection */
5110 43
        foreach ($needles as &$needle) {
5111 43
            if (!$needle) {
5112 1
                return false;
5113
            }
5114
5115 42
            if ($case_sensitive) {
5116 22
                return \strpos($haystack, $needle) !== false;
5117
            }
5118
5119 20
            return \mb_stripos($haystack, $needle) !== false;
5120
        }
5121
5122
        return true;
5123
    }
5124
5125
    /**
5126
     * Returns true if the string contains any $needles, false otherwise. By
5127
     * default the comparison is case-sensitive, but can be made insensitive by
5128
     * setting $case_sensitive to false.
5129
     *
5130
     * @param string $haystack       <p>The input string.</p>
5131
     * @param array  $needles        <p>SubStrings to look for.</p>
5132
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5133
     *
5134
     * @return bool
5135
     *              Whether or not $str contains $needle
5136
     */
5137
    public static function str_contains_any(
5138
        string $haystack,
5139
        array $needles,
5140
        bool $case_sensitive = true
5141
    ): bool {
5142 46
        if ($haystack === '' || $needles === []) {
5143 1
            return false;
5144
        }
5145
5146
        /** @noinspection LoopWhichDoesNotLoopInspection */
5147 45
        foreach ($needles as &$needle) {
5148 45
            if (!$needle) {
5149
                continue;
5150
            }
5151
5152 45
            if ($case_sensitive) {
5153 25
                if (\strpos($haystack, $needle) !== false) {
5154 14
                    return true;
5155
                }
5156
5157 13
                continue;
5158
            }
5159
5160 20
            if (\mb_stripos($haystack, $needle) !== false) {
5161 20
                return true;
5162
            }
5163
        }
5164
5165 19
        return false;
5166
    }
5167
5168
    /**
5169
     * Returns a lowercase and trimmed string separated by dashes. Dashes are
5170
     * inserted before uppercase characters (with the exception of the first
5171
     * character of the string), and in place of spaces as well as underscores.
5172
     *
5173
     * @param string $str      <p>The input string.</p>
5174
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5175
     *
5176
     * @return string
5177
     */
5178
    public static function str_dasherize(string $str, string $encoding = 'UTF-8'): string
5179
    {
5180 19
        return self::str_delimit($str, '-', $encoding);
5181
    }
5182
5183
    /**
5184
     * Returns a lowercase and trimmed string separated by the given delimiter.
5185
     * Delimiters are inserted before uppercase characters (with the exception
5186
     * of the first character of the string), and in place of spaces, dashes,
5187
     * and underscores. Alpha delimiters are not converted to lowercase.
5188
     *
5189
     * @param string      $str                           <p>The input string.</p>
5190
     * @param string      $delimiter                     <p>Sequence used to separate parts of the string.</p>
5191
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
5192
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5193
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5194
     *                                                   tr</p>
5195
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
5196
     *                                                   ß</p>
5197
     *
5198
     * @return string
5199
     */
5200
    public static function str_delimit(
5201
        string $str,
5202
        string $delimiter,
5203
        string $encoding = 'UTF-8',
5204
        bool $clean_utf8 = false,
5205
        string $lang = null,
5206
        bool $try_to_keep_the_string_length = false
5207
    ): string {
5208 49
        if (self::$SUPPORT['mbstring'] === true) {
5209
            /** @noinspection PhpComposerExtensionStubsInspection */
5210 49
            $str = (string) \mb_ereg_replace('\\B(\\p{Lu})', '-\1', \trim($str));
5211
5212 49
            $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5213 49
            if ($use_mb_functions === true && $encoding === 'UTF-8') {
5214 22
                $str = \mb_strtolower($str);
5215
            } else {
5216 27
                $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5217
            }
5218
5219
            /** @noinspection PhpComposerExtensionStubsInspection */
5220 49
            return (string) \mb_ereg_replace('[\\-_\\s]+', $delimiter, $str);
5221
        }
5222
5223
        $str = (string) \preg_replace('/\\B(\\p{Lu})/u', '-\1', \trim($str));
5224
5225
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5226
        if ($use_mb_functions === true && $encoding === 'UTF-8') {
5227
            $str = \mb_strtolower($str);
5228
        } else {
5229
            $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5230
        }
5231
5232
        return (string) \preg_replace('/[\\-_\\s]+/u', $delimiter, $str);
5233
    }
5234
5235
    /**
5236
     * Optimized "mb_detect_encoding()"-function -> with support for UTF-16 and UTF-32.
5237
     *
5238
     * @param string $str <p>The input string.</p>
5239
     *
5240
     * @return false|string
5241
     *                      The detected string-encoding e.g. UTF-8 or UTF-16BE,<br>
5242
     *                      otherwise it will return false e.g. for BINARY or not detected encoding.
5243
     */
5244
    public static function str_detect_encoding($str)
5245
    {
5246
        // init
5247 30
        $str = (string) $str;
5248
5249
        //
5250
        // 1.) check binary strings (010001001...) like UTF-16 / UTF-32 / PDF / Images / ...
5251
        //
5252
5253 30
        if (self::is_binary($str, true) === true) {
5254 11
            $is_utf32 = self::is_utf32($str, false);
5255 11
            if ($is_utf32 === 1) {
5256
                return 'UTF-32LE';
5257
            }
5258 11
            if ($is_utf32 === 2) {
5259 1
                return 'UTF-32BE';
5260
            }
5261
5262 11
            $is_utf16 = self::is_utf16($str, false);
5263 11
            if ($is_utf16 === 1) {
5264 3
                return 'UTF-16LE';
5265
            }
5266 11
            if ($is_utf16 === 2) {
5267 2
                return 'UTF-16BE';
5268
            }
5269
5270
            // is binary but not "UTF-16" or "UTF-32"
5271 9
            return false;
5272
        }
5273
5274
        //
5275
        // 2.) simple check for ASCII chars
5276
        //
5277
5278 26
        if (ASCII::is_ascii($str) === true) {
5279 10
            return 'ASCII';
5280
        }
5281
5282
        //
5283
        // 3.) simple check for UTF-8 chars
5284
        //
5285
5286 26
        if (self::is_utf8_string($str) === true) {
5287 19
            return 'UTF-8';
5288
        }
5289
5290
        //
5291
        // 4.) check via "mb_detect_encoding()"
5292
        //
5293
        // INFO: UTF-16, UTF-32, UCS2 and UCS4, encoding detection will fail always with "mb_detect_encoding()"
5294
5295
        $encoding_detecting_order = [
5296 15
            'ISO-8859-1',
5297
            'ISO-8859-2',
5298
            'ISO-8859-3',
5299
            'ISO-8859-4',
5300
            'ISO-8859-5',
5301
            'ISO-8859-6',
5302
            'ISO-8859-7',
5303
            'ISO-8859-8',
5304
            'ISO-8859-9',
5305
            'ISO-8859-10',
5306
            'ISO-8859-13',
5307
            'ISO-8859-14',
5308
            'ISO-8859-15',
5309
            'ISO-8859-16',
5310
            'WINDOWS-1251',
5311
            'WINDOWS-1252',
5312
            'WINDOWS-1254',
5313
            'CP932',
5314
            'CP936',
5315
            'CP950',
5316
            'CP866',
5317
            'CP850',
5318
            'CP51932',
5319
            'CP50220',
5320
            'CP50221',
5321
            'CP50222',
5322
            'ISO-2022-JP',
5323
            'ISO-2022-KR',
5324
            'JIS',
5325
            'JIS-ms',
5326
            'EUC-CN',
5327
            'EUC-JP',
5328
        ];
5329
5330 15
        if (self::$SUPPORT['mbstring'] === true) {
5331
            // info: do not use the symfony polyfill here
5332 15
            $encoding = \mb_detect_encoding($str, $encoding_detecting_order, true);
5333 15
            if ($encoding) {
5334 15
                return $encoding;
5335
            }
5336
        }
5337
5338
        //
5339
        // 5.) check via "iconv()"
5340
        //
5341
5342
        if (self::$ENCODINGS === null) {
5343
            self::$ENCODINGS = self::getData('encodings');
5344
        }
5345
5346
        foreach (self::$ENCODINGS as $encoding_tmp) {
5347
            // INFO: //IGNORE but still throw notice
5348
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
5349
            if ((string) @\iconv($encoding_tmp, $encoding_tmp . '//IGNORE', $str) === $str) {
5350
                return $encoding_tmp;
5351
            }
5352
        }
5353
5354
        return false;
5355
    }
5356
5357
    /**
5358
     * alias for "UTF8::str_ends_with()"
5359
     *
5360
     * @param string $haystack
5361
     * @param string $needle
5362
     *
5363
     * @return bool
5364
     *
5365
     * @see UTF8::str_ends_with()
5366
     * @deprecated <p>please use "UTF8::str_ends_with()"</p>
5367
     */
5368
    public static function str_ends(string $haystack, string $needle): bool
5369
    {
5370
        return self::str_ends_with($haystack, $needle);
5371
    }
5372
5373
    /**
5374
     * Check if the string ends with the given substring.
5375
     *
5376
     * @param string $haystack <p>The string to search in.</p>
5377
     * @param string $needle   <p>The substring to search for.</p>
5378
     *
5379
     * @return bool
5380
     */
5381
    public static function str_ends_with(string $haystack, string $needle): bool
5382
    {
5383 9
        if ($needle === '') {
5384 2
            return true;
5385
        }
5386
5387 9
        if ($haystack === '') {
5388
            return false;
5389
        }
5390
5391 9
        return \substr($haystack, -\strlen($needle)) === $needle;
5392
    }
5393
5394
    /**
5395
     * Returns true if the string ends with any of $substrings, false otherwise.
5396
     *
5397
     * - case-sensitive
5398
     *
5399
     * @param string   $str        <p>The input string.</p>
5400
     * @param string[] $substrings <p>Substrings to look for.</p>
5401
     *
5402
     * @return bool whether or not $str ends with $substring
5403
     */
5404
    public static function str_ends_with_any(string $str, array $substrings): bool
5405
    {
5406 7
        if ($substrings === []) {
5407
            return false;
5408
        }
5409
5410 7
        foreach ($substrings as &$substring) {
5411 7
            if (\substr($str, -\strlen($substring)) === $substring) {
5412 7
                return true;
5413
            }
5414
        }
5415
5416 6
        return false;
5417
    }
5418
5419
    /**
5420
     * Ensures that the string begins with $substring. If it doesn't, it's
5421
     * prepended.
5422
     *
5423
     * @param string $str       <p>The input string.</p>
5424
     * @param string $substring <p>The substring to add if not present.</p>
5425
     *
5426
     * @return string
5427
     */
5428
    public static function str_ensure_left(string $str, string $substring): string
5429
    {
5430
        if (
5431 10
            $substring !== ''
5432
            &&
5433 10
            \strpos($str, $substring) === 0
5434
        ) {
5435 6
            return $str;
5436
        }
5437
5438 4
        return $substring . $str;
5439
    }
5440
5441
    /**
5442
     * Ensures that the string ends with $substring. If it doesn't, it's appended.
5443
     *
5444
     * @param string $str       <p>The input string.</p>
5445
     * @param string $substring <p>The substring to add if not present.</p>
5446
     *
5447
     * @return string
5448
     */
5449
    public static function str_ensure_right(string $str, string $substring): string
5450
    {
5451
        if (
5452 10
            $str === ''
5453
            ||
5454 10
            $substring === ''
5455
            ||
5456 10
            \substr($str, -\strlen($substring)) !== $substring
5457
        ) {
5458 4
            $str .= $substring;
5459
        }
5460
5461 10
        return $str;
5462
    }
5463
5464
    /**
5465
     * Capitalizes the first word of the string, replaces underscores with
5466
     * spaces, and strips '_id'.
5467
     *
5468
     * @param string $str
5469
     *
5470
     * @return string
5471
     */
5472
    public static function str_humanize($str): string
5473
    {
5474 3
        $str = \str_replace(
5475
            [
5476 3
                '_id',
5477
                '_',
5478
            ],
5479
            [
5480 3
                '',
5481
                ' ',
5482
            ],
5483 3
            $str
5484
        );
5485
5486 3
        return self::ucfirst(\trim($str));
5487
    }
5488
5489
    /**
5490
     * alias for "UTF8::str_istarts_with()"
5491
     *
5492
     * @param string $haystack
5493
     * @param string $needle
5494
     *
5495
     * @return bool
5496
     *
5497
     * @see UTF8::str_istarts_with()
5498
     * @deprecated <p>please use "UTF8::str_istarts_with()"</p>
5499
     */
5500
    public static function str_ibegins(string $haystack, string $needle): bool
5501
    {
5502
        return self::str_istarts_with($haystack, $needle);
5503
    }
5504
5505
    /**
5506
     * alias for "UTF8::str_iends_with()"
5507
     *
5508
     * @param string $haystack
5509
     * @param string $needle
5510
     *
5511
     * @return bool
5512
     *
5513
     * @see UTF8::str_iends_with()
5514
     * @deprecated <p>please use "UTF8::str_iends_with()"</p>
5515
     */
5516
    public static function str_iends(string $haystack, string $needle): bool
5517
    {
5518
        return self::str_iends_with($haystack, $needle);
5519
    }
5520
5521
    /**
5522
     * Check if the string ends with the given substring, case-insensitive.
5523
     *
5524
     * @param string $haystack <p>The string to search in.</p>
5525
     * @param string $needle   <p>The substring to search for.</p>
5526
     *
5527
     * @return bool
5528
     */
5529
    public static function str_iends_with(string $haystack, string $needle): bool
5530
    {
5531 12
        if ($needle === '') {
5532 2
            return true;
5533
        }
5534
5535 12
        if ($haystack === '') {
5536
            return false;
5537
        }
5538
5539 12
        return self::strcasecmp(\substr($haystack, -\strlen($needle)), $needle) === 0;
5540
    }
5541
5542
    /**
5543
     * Returns true if the string ends with any of $substrings, false otherwise.
5544
     *
5545
     * - case-insensitive
5546
     *
5547
     * @param string   $str        <p>The input string.</p>
5548
     * @param string[] $substrings <p>Substrings to look for.</p>
5549
     *
5550
     * @return bool
5551
     *              <p>Whether or not $str ends with $substring.</p>
5552
     */
5553
    public static function str_iends_with_any(string $str, array $substrings): bool
5554
    {
5555 4
        if ($substrings === []) {
5556
            return false;
5557
        }
5558
5559 4
        foreach ($substrings as &$substring) {
5560 4
            if (self::str_iends_with($str, $substring)) {
5561 4
                return true;
5562
            }
5563
        }
5564
5565
        return false;
5566
    }
5567
5568
    /**
5569
     * Returns the index of the first occurrence of $needle in the string,
5570
     * and false if not found. Accepts an optional offset from which to begin
5571
     * the search.
5572
     *
5573
     * @param string $str      <p>The input string.</p>
5574
     * @param string $needle   <p>Substring to look for.</p>
5575
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5576
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5577
     *
5578
     * @return false|int
5579
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5580
     *
5581
     * @see UTF8::stripos()
5582
     * @deprecated <p>please use "UTF8::stripos()"</p>
5583
     */
5584
    public static function str_iindex_first(
5585
        string $str,
5586
        string $needle,
5587
        int $offset = 0,
5588
        string $encoding = 'UTF-8'
5589
    ) {
5590
        return self::stripos(
5591
            $str,
5592
            $needle,
5593
            $offset,
5594
            $encoding
5595
        );
5596
    }
5597
5598
    /**
5599
     * Returns the index of the last occurrence of $needle in the string,
5600
     * and false if not found. Accepts an optional offset from which to begin
5601
     * the search. Offsets may be negative to count from the last character
5602
     * in the string.
5603
     *
5604
     * @param string $str      <p>The input string.</p>
5605
     * @param string $needle   <p>Substring to look for.</p>
5606
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5607
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5608
     *
5609
     * @return false|int
5610
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5611
     *
5612
     * @see UTF8::strripos()
5613
     * @deprecated <p>please use "UTF8::strripos()"</p>
5614
     */
5615
    public static function str_iindex_last(
5616
        string $str,
5617
        string $needle,
5618
        int $offset = 0,
5619
        string $encoding = 'UTF-8'
5620
    ) {
5621
        return self::strripos(
5622
            $str,
5623
            $needle,
5624
            $offset,
5625
            $encoding
5626
        );
5627
    }
5628
5629
    /**
5630
     * Returns the index of the first occurrence of $needle in the string,
5631
     * and false if not found. Accepts an optional offset from which to begin
5632
     * the search.
5633
     *
5634
     * @param string $str      <p>The input string.</p>
5635
     * @param string $needle   <p>Substring to look for.</p>
5636
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5637
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5638
     *
5639
     * @return false|int
5640
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5641
     *
5642
     * @see UTF8::strpos()
5643
     * @deprecated <p>please use "UTF8::strpos()"</p>
5644
     */
5645
    public static function str_index_first(
5646
        string $str,
5647
        string $needle,
5648
        int $offset = 0,
5649
        string $encoding = 'UTF-8'
5650
    ) {
5651 10
        return self::strpos(
5652 10
            $str,
5653 10
            $needle,
5654 10
            $offset,
5655 10
            $encoding
5656
        );
5657
    }
5658
5659
    /**
5660
     * Returns the index of the last occurrence of $needle in the string,
5661
     * and false if not found. Accepts an optional offset from which to begin
5662
     * the search. Offsets may be negative to count from the last character
5663
     * in the string.
5664
     *
5665
     * @param string $str      <p>The input string.</p>
5666
     * @param string $needle   <p>Substring to look for.</p>
5667
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
5668
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5669
     *
5670
     * @return false|int
5671
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
5672
     *
5673
     * @see UTF8::strrpos()
5674
     * @deprecated <p>please use "UTF8::strrpos()"</p>
5675
     */
5676
    public static function str_index_last(
5677
        string $str,
5678
        string $needle,
5679
        int $offset = 0,
5680
        string $encoding = 'UTF-8'
5681
    ) {
5682 10
        return self::strrpos(
5683 10
            $str,
5684 10
            $needle,
5685 10
            $offset,
5686 10
            $encoding
5687
        );
5688
    }
5689
5690
    /**
5691
     * Inserts $substring into the string at the $index provided.
5692
     *
5693
     * @param string $str       <p>The input string.</p>
5694
     * @param string $substring <p>String to be inserted.</p>
5695
     * @param int    $index     <p>The index at which to insert the substring.</p>
5696
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
5697
     *
5698
     * @return string
5699
     */
5700
    public static function str_insert(
5701
        string $str,
5702
        string $substring,
5703
        int $index,
5704
        string $encoding = 'UTF-8'
5705
    ): string {
5706 8
        if ($encoding === 'UTF-8') {
5707 4
            $len = (int) \mb_strlen($str);
5708 4
            if ($index > $len) {
5709
                return $str;
5710
            }
5711
5712
            /** @noinspection UnnecessaryCastingInspection */
5713 4
            return (string) \mb_substr($str, 0, $index) .
5714 4
                   $substring .
5715 4
                   (string) \mb_substr($str, $index, $len);
5716
        }
5717
5718 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
5719
5720 4
        $len = (int) self::strlen($str, $encoding);
5721 4
        if ($index > $len) {
5722 1
            return $str;
5723
        }
5724
5725 3
        return ((string) self::substr($str, 0, $index, $encoding)) .
5726 3
               $substring .
5727 3
               ((string) self::substr($str, $index, $len, $encoding));
5728
    }
5729
5730
    /**
5731
     * Case-insensitive and UTF-8 safe version of <function>str_replace</function>.
5732
     *
5733
     * @see http://php.net/manual/en/function.str-ireplace.php
5734
     *
5735
     * @param mixed $search  <p>
5736
     *                       Every replacement with search array is
5737
     *                       performed on the result of previous replacement.
5738
     *                       </p>
5739
     * @param mixed $replace <p>
5740
     *                       </p>
5741
     * @param mixed $subject <p>
5742
     *                       If subject is an array, then the search and
5743
     *                       replace is performed with every entry of
5744
     *                       subject, and the return value is an array as
5745
     *                       well.
5746
     *                       </p>
5747
     * @param int   $count   [optional] <p>
5748
     *                       The number of matched and replaced needles will
5749
     *                       be returned in count which is passed by
5750
     *                       reference.
5751
     *                       </p>
5752
     *
5753
     * @return mixed a string or an array of replacements
5754
     */
5755
    public static function str_ireplace($search, $replace, $subject, &$count = null)
5756
    {
5757 29
        $search = (array) $search;
5758
5759
        /** @noinspection AlterInForeachInspection */
5760 29
        foreach ($search as &$s) {
5761 29
            $s = (string) $s;
5762 29
            if ($s === '') {
5763 6
                $s = '/^(?<=.)$/';
5764
            } else {
5765 29
                $s = '/' . \preg_quote($s, '/') . '/ui';
5766
            }
5767
        }
5768
5769 29
        $subject = \preg_replace($search, $replace, $subject, -1, $replace);
5770 29
        $count = $replace; // used as reference parameter
5771
5772 29
        return $subject;
5773
    }
5774
5775
    /**
5776
     * Replaces $search from the beginning of string with $replacement.
5777
     *
5778
     * @param string $str         <p>The input string.</p>
5779
     * @param string $search      <p>The string to search for.</p>
5780
     * @param string $replacement <p>The replacement.</p>
5781
     *
5782
     * @return string string after the replacements
5783
     */
5784
    public static function str_ireplace_beginning(string $str, string $search, string $replacement): string
5785
    {
5786 17
        if ($str === '') {
5787 4
            if ($replacement === '') {
5788 2
                return '';
5789
            }
5790
5791 2
            if ($search === '') {
5792 2
                return $replacement;
5793
            }
5794
        }
5795
5796 13
        if ($search === '') {
5797 2
            return $str . $replacement;
5798
        }
5799
5800 11
        if (\stripos($str, $search) === 0) {
5801 10
            return $replacement . \substr($str, \strlen($search));
5802
        }
5803
5804 1
        return $str;
5805
    }
5806
5807
    /**
5808
     * Replaces $search from the ending of string with $replacement.
5809
     *
5810
     * @param string $str         <p>The input string.</p>
5811
     * @param string $search      <p>The string to search for.</p>
5812
     * @param string $replacement <p>The replacement.</p>
5813
     *
5814
     * @return string string after the replacements
5815
     */
5816
    public static function str_ireplace_ending(string $str, string $search, string $replacement): string
5817
    {
5818 17
        if ($str === '') {
5819 4
            if ($replacement === '') {
5820 2
                return '';
5821
            }
5822
5823 2
            if ($search === '') {
5824 2
                return $replacement;
5825
            }
5826
        }
5827
5828 13
        if ($search === '') {
5829 2
            return $str . $replacement;
5830
        }
5831
5832 11
        if (\stripos($str, $search, \strlen($str) - \strlen($search)) !== false) {
5833 9
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
5834
        }
5835
5836 11
        return $str;
5837
    }
5838
5839
    /**
5840
     * Check if the string starts with the given substring, case-insensitive.
5841
     *
5842
     * @param string $haystack <p>The string to search in.</p>
5843
     * @param string $needle   <p>The substring to search for.</p>
5844
     *
5845
     * @return bool
5846
     */
5847
    public static function str_istarts_with(string $haystack, string $needle): bool
5848
    {
5849 12
        if ($needle === '') {
5850 2
            return true;
5851
        }
5852
5853 12
        if ($haystack === '') {
5854
            return false;
5855
        }
5856
5857 12
        return self::stripos($haystack, $needle) === 0;
5858
    }
5859
5860
    /**
5861
     * Returns true if the string begins with any of $substrings, false otherwise.
5862
     *
5863
     * - case-insensitive
5864
     *
5865
     * @param string $str        <p>The input string.</p>
5866
     * @param array  $substrings <p>Substrings to look for.</p>
5867
     *
5868
     * @return bool whether or not $str starts with $substring
5869
     */
5870
    public static function str_istarts_with_any(string $str, array $substrings): bool
5871
    {
5872 4
        if ($str === '') {
5873
            return false;
5874
        }
5875
5876 4
        if ($substrings === []) {
5877
            return false;
5878
        }
5879
5880 4
        foreach ($substrings as &$substring) {
5881 4
            if (self::str_istarts_with($str, $substring)) {
5882 4
                return true;
5883
            }
5884
        }
5885
5886
        return false;
5887
    }
5888
5889
    /**
5890
     * Gets the substring after the first occurrence of a separator.
5891
     *
5892
     * @param string $str       <p>The input string.</p>
5893
     * @param string $separator <p>The string separator.</p>
5894
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5895
     *
5896
     * @return string
5897
     */
5898
    public static function str_isubstr_after_first_separator(
5899
        string $str,
5900
        string $separator,
5901
        string $encoding = 'UTF-8'
5902
    ): string {
5903 1
        if ($separator === '' || $str === '') {
5904 1
            return '';
5905
        }
5906
5907 1
        $offset = self::stripos($str, $separator);
5908 1
        if ($offset === false) {
5909 1
            return '';
5910
        }
5911
5912 1
        if ($encoding === 'UTF-8') {
5913 1
            return (string) \mb_substr(
5914 1
                $str,
5915 1
                $offset + (int) \mb_strlen($separator)
5916
            );
5917
        }
5918
5919
        return (string) self::substr(
5920
            $str,
5921
            $offset + (int) self::strlen($separator, $encoding),
5922
            null,
5923
            $encoding
5924
        );
5925
    }
5926
5927
    /**
5928
     * Gets the substring after the last occurrence of a separator.
5929
     *
5930
     * @param string $str       <p>The input string.</p>
5931
     * @param string $separator <p>The string separator.</p>
5932
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5933
     *
5934
     * @return string
5935
     */
5936
    public static function str_isubstr_after_last_separator(
5937
        string $str,
5938
        string $separator,
5939
        string $encoding = 'UTF-8'
5940
    ): string {
5941 1
        if ($separator === '' || $str === '') {
5942 1
            return '';
5943
        }
5944
5945 1
        $offset = self::strripos($str, $separator);
5946 1
        if ($offset === false) {
5947 1
            return '';
5948
        }
5949
5950 1
        if ($encoding === 'UTF-8') {
5951 1
            return (string) \mb_substr(
5952 1
                $str,
5953 1
                $offset + (int) self::strlen($separator)
5954
            );
5955
        }
5956
5957
        return (string) self::substr(
5958
            $str,
5959
            $offset + (int) self::strlen($separator, $encoding),
5960
            null,
5961
            $encoding
5962
        );
5963
    }
5964
5965
    /**
5966
     * Gets the substring before the first occurrence of a separator.
5967
     *
5968
     * @param string $str       <p>The input string.</p>
5969
     * @param string $separator <p>The string separator.</p>
5970
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5971
     *
5972
     * @return string
5973
     */
5974
    public static function str_isubstr_before_first_separator(
5975
        string $str,
5976
        string $separator,
5977
        string $encoding = 'UTF-8'
5978
    ): string {
5979 1
        if ($separator === '' || $str === '') {
5980 1
            return '';
5981
        }
5982
5983 1
        $offset = self::stripos($str, $separator);
5984 1
        if ($offset === false) {
5985 1
            return '';
5986
        }
5987
5988 1
        if ($encoding === 'UTF-8') {
5989 1
            return (string) \mb_substr($str, 0, $offset);
5990
        }
5991
5992
        return (string) self::substr($str, 0, $offset, $encoding);
5993
    }
5994
5995
    /**
5996
     * Gets the substring before the last occurrence of a separator.
5997
     *
5998
     * @param string $str       <p>The input string.</p>
5999
     * @param string $separator <p>The string separator.</p>
6000
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6001
     *
6002
     * @return string
6003
     */
6004
    public static function str_isubstr_before_last_separator(
6005
        string $str,
6006
        string $separator,
6007
        string $encoding = 'UTF-8'
6008
    ): string {
6009 1
        if ($separator === '' || $str === '') {
6010 1
            return '';
6011
        }
6012
6013 1
        if ($encoding === 'UTF-8') {
6014 1
            $offset = \mb_strripos($str, $separator);
6015 1
            if ($offset === false) {
6016 1
                return '';
6017
            }
6018
6019 1
            return (string) \mb_substr($str, 0, $offset);
6020
        }
6021
6022
        $offset = self::strripos($str, $separator, 0, $encoding);
6023
        if ($offset === false) {
6024
            return '';
6025
        }
6026
6027
        return (string) self::substr($str, 0, $offset, $encoding);
6028
    }
6029
6030
    /**
6031
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
6032
     *
6033
     * @param string $str           <p>The input string.</p>
6034
     * @param string $needle        <p>The string to look for.</p>
6035
     * @param bool   $before_needle [optional] <p>Default: false</p>
6036
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6037
     *
6038
     * @return string
6039
     */
6040
    public static function str_isubstr_first(
6041
        string $str,
6042
        string $needle,
6043
        bool $before_needle = false,
6044
        string $encoding = 'UTF-8'
6045
    ): string {
6046
        if (
6047 2
            $needle === ''
6048
            ||
6049 2
            $str === ''
6050
        ) {
6051 2
            return '';
6052
        }
6053
6054 2
        $part = self::stristr(
6055 2
            $str,
6056 2
            $needle,
6057 2
            $before_needle,
6058 2
            $encoding
6059
        );
6060 2
        if ($part === false) {
6061 2
            return '';
6062
        }
6063
6064 2
        return $part;
6065
    }
6066
6067
    /**
6068
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
6069
     *
6070
     * @param string $str           <p>The input string.</p>
6071
     * @param string $needle        <p>The string to look for.</p>
6072
     * @param bool   $before_needle [optional] <p>Default: false</p>
6073
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6074
     *
6075
     * @return string
6076
     */
6077
    public static function str_isubstr_last(
6078
        string $str,
6079
        string $needle,
6080
        bool $before_needle = false,
6081
        string $encoding = 'UTF-8'
6082
    ): string {
6083
        if (
6084 1
            $needle === ''
6085
            ||
6086 1
            $str === ''
6087
        ) {
6088 1
            return '';
6089
        }
6090
6091 1
        $part = self::strrichr(
6092 1
            $str,
6093 1
            $needle,
6094 1
            $before_needle,
6095 1
            $encoding
6096
        );
6097 1
        if ($part === false) {
6098 1
            return '';
6099
        }
6100
6101 1
        return $part;
6102
    }
6103
6104
    /**
6105
     * Returns the last $n characters of the string.
6106
     *
6107
     * @param string $str      <p>The input string.</p>
6108
     * @param int    $n        <p>Number of characters to retrieve from the end.</p>
6109
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6110
     *
6111
     * @return string
6112
     */
6113
    public static function str_last_char(
6114
        string $str,
6115
        int $n = 1,
6116
        string $encoding = 'UTF-8'
6117
    ): string {
6118 12
        if ($str === '' || $n <= 0) {
6119 4
            return '';
6120
        }
6121
6122 8
        if ($encoding === 'UTF-8') {
6123 4
            return (string) \mb_substr($str, -$n);
6124
        }
6125
6126 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6127
6128 4
        return (string) self::substr($str, -$n, null, $encoding);
6129
    }
6130
6131
    /**
6132
     * Limit the number of characters in a string.
6133
     *
6134
     * @param string $str        <p>The input string.</p>
6135
     * @param int    $length     [optional] <p>Default: 100</p>
6136
     * @param string $str_add_on [optional] <p>Default: …</p>
6137
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6138
     *
6139
     * @return string
6140
     */
6141
    public static function str_limit(
6142
        string $str,
6143
        int $length = 100,
6144
        string $str_add_on = '…',
6145
        string $encoding = 'UTF-8'
6146
    ): string {
6147 2
        if ($str === '' || $length <= 0) {
6148 2
            return '';
6149
        }
6150
6151 2
        if ($encoding === 'UTF-8') {
6152 2
            if ((int) \mb_strlen($str) <= $length) {
6153 2
                return $str;
6154
            }
6155
6156
            /** @noinspection UnnecessaryCastingInspection */
6157 2
            return (string) \mb_substr($str, 0, $length - (int) self::strlen($str_add_on)) . $str_add_on;
6158
        }
6159
6160
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6161
6162
        if ((int) self::strlen($str, $encoding) <= $length) {
6163
            return $str;
6164
        }
6165
6166
        return ((string) self::substr($str, 0, $length - (int) self::strlen($str_add_on), $encoding)) . $str_add_on;
6167
    }
6168
6169
    /**
6170
     * Limit the number of characters in a string, but also after the next word.
6171
     *
6172
     * @param string $str        <p>The input string.</p>
6173
     * @param int    $length     [optional] <p>Default: 100</p>
6174
     * @param string $str_add_on [optional] <p>Default: …</p>
6175
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6176
     *
6177
     * @return string
6178
     */
6179
    public static function str_limit_after_word(
6180
        string $str,
6181
        int $length = 100,
6182
        string $str_add_on = '…',
6183
        string $encoding = 'UTF-8'
6184
    ): string {
6185 6
        if ($str === '' || $length <= 0) {
6186 2
            return '';
6187
        }
6188
6189 6
        if ($encoding === 'UTF-8') {
6190
            /** @noinspection UnnecessaryCastingInspection */
6191 2
            if ((int) \mb_strlen($str) <= $length) {
6192 2
                return $str;
6193
            }
6194
6195 2
            if (\mb_substr($str, $length - 1, 1) === ' ') {
6196 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6197
            }
6198
6199 2
            $str = \mb_substr($str, 0, $length);
6200
6201 2
            $array = \explode(' ', $str);
6202 2
            \array_pop($array);
6203 2
            $new_str = \implode(' ', $array);
6204
6205 2
            if ($new_str === '') {
6206 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6207
            }
6208
        } else {
6209 4
            if ((int) self::strlen($str, $encoding) <= $length) {
6210
                return $str;
6211
            }
6212
6213 4
            if (self::substr($str, $length - 1, 1, $encoding) === ' ') {
6214 3
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6215
            }
6216
6217
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6218 1
            $str = self::substr($str, 0, $length, $encoding);
6219
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6220 1
            if ($str === false) {
6221
                return '' . $str_add_on;
6222
            }
6223
6224 1
            $array = \explode(' ', $str);
6225 1
            \array_pop($array);
6226 1
            $new_str = \implode(' ', $array);
6227
6228 1
            if ($new_str === '') {
6229
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6230
            }
6231
        }
6232
6233 3
        return $new_str . $str_add_on;
6234
    }
6235
6236
    /**
6237
     * Returns the longest common prefix between the $str1 and $str2.
6238
     *
6239
     * @param string $str1     <p>The input sting.</p>
6240
     * @param string $str2     <p>Second string for comparison.</p>
6241
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6242
     *
6243
     * @return string
6244
     */
6245
    public static function str_longest_common_prefix(
6246
        string $str1,
6247
        string $str2,
6248
        string $encoding = 'UTF-8'
6249
    ): string {
6250
        // init
6251 10
        $longest_common_prefix = '';
6252
6253 10
        if ($encoding === 'UTF-8') {
6254 5
            $max_length = (int) \min(
6255 5
                \mb_strlen($str1),
6256 5
                \mb_strlen($str2)
6257
            );
6258
6259 5
            for ($i = 0; $i < $max_length; ++$i) {
6260 4
                $char = \mb_substr($str1, $i, 1);
6261
6262
                if (
6263 4
                    $char !== false
6264
                    &&
6265 4
                    $char === \mb_substr($str2, $i, 1)
6266
                ) {
6267 3
                    $longest_common_prefix .= $char;
6268
                } else {
6269 3
                    break;
6270
                }
6271
            }
6272
        } else {
6273 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6274
6275 5
            $max_length = (int) \min(
6276 5
                self::strlen($str1, $encoding),
6277 5
                self::strlen($str2, $encoding)
6278
            );
6279
6280 5
            for ($i = 0; $i < $max_length; ++$i) {
6281 4
                $char = self::substr($str1, $i, 1, $encoding);
6282
6283
                if (
6284 4
                    $char !== false
6285
                    &&
6286 4
                    $char === self::substr($str2, $i, 1, $encoding)
6287
                ) {
6288 3
                    $longest_common_prefix .= $char;
6289
                } else {
6290 3
                    break;
6291
                }
6292
            }
6293
        }
6294
6295 10
        return $longest_common_prefix;
6296
    }
6297
6298
    /**
6299
     * Returns the longest common substring between the $str1 and $str2.
6300
     * In the case of ties, it returns that which occurs first.
6301
     *
6302
     * @param string $str1
6303
     * @param string $str2     <p>Second string for comparison.</p>
6304
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6305
     *
6306
     * @return string string with its $str being the longest common substring
6307
     */
6308
    public static function str_longest_common_substring(
6309
        string $str1,
6310
        string $str2,
6311
        string $encoding = 'UTF-8'
6312
    ): string {
6313 11
        if ($str1 === '' || $str2 === '') {
6314 2
            return '';
6315
        }
6316
6317
        // Uses dynamic programming to solve
6318
        // http://en.wikipedia.org/wiki/Longest_common_substring_problem
6319
6320 9
        if ($encoding === 'UTF-8') {
6321 4
            $str_length = (int) \mb_strlen($str1);
6322 4
            $other_length = (int) \mb_strlen($str2);
6323
        } else {
6324 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6325
6326 5
            $str_length = (int) self::strlen($str1, $encoding);
6327 5
            $other_length = (int) self::strlen($str2, $encoding);
6328
        }
6329
6330
        // Return if either string is empty
6331 9
        if ($str_length === 0 || $other_length === 0) {
6332
            return '';
6333
        }
6334
6335 9
        $len = 0;
6336 9
        $end = 0;
6337 9
        $table = \array_fill(
6338 9
            0,
6339 9
            $str_length + 1,
6340 9
            \array_fill(0, $other_length + 1, 0)
6341
        );
6342
6343 9
        if ($encoding === 'UTF-8') {
6344 9
            for ($i = 1; $i <= $str_length; ++$i) {
6345 9
                for ($j = 1; $j <= $other_length; ++$j) {
6346 9
                    $str_char = \mb_substr($str1, $i - 1, 1);
6347 9
                    $other_char = \mb_substr($str2, $j - 1, 1);
6348
6349 9
                    if ($str_char === $other_char) {
6350 8
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6351 8
                        if ($table[$i][$j] > $len) {
6352 8
                            $len = $table[$i][$j];
6353 8
                            $end = $i;
6354
                        }
6355
                    } else {
6356 9
                        $table[$i][$j] = 0;
6357
                    }
6358
                }
6359
            }
6360
        } else {
6361
            for ($i = 1; $i <= $str_length; ++$i) {
6362
                for ($j = 1; $j <= $other_length; ++$j) {
6363
                    $str_char = self::substr($str1, $i - 1, 1, $encoding);
6364
                    $other_char = self::substr($str2, $j - 1, 1, $encoding);
6365
6366
                    if ($str_char === $other_char) {
6367
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6368
                        if ($table[$i][$j] > $len) {
6369
                            $len = $table[$i][$j];
6370
                            $end = $i;
6371
                        }
6372
                    } else {
6373
                        $table[$i][$j] = 0;
6374
                    }
6375
                }
6376
            }
6377
        }
6378
6379 9
        if ($encoding === 'UTF-8') {
6380 9
            return (string) \mb_substr($str1, $end - $len, $len);
6381
        }
6382
6383
        return (string) self::substr($str1, $end - $len, $len, $encoding);
6384
    }
6385
6386
    /**
6387
     * Returns the longest common suffix between the $str1 and $str2.
6388
     *
6389
     * @param string $str1
6390
     * @param string $str2     <p>Second string for comparison.</p>
6391
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6392
     *
6393
     * @return string
6394
     */
6395
    public static function str_longest_common_suffix(
6396
        string $str1,
6397
        string $str2,
6398
        string $encoding = 'UTF-8'
6399
    ): string {
6400 10
        if ($str1 === '' || $str2 === '') {
6401 2
            return '';
6402
        }
6403
6404 8
        if ($encoding === 'UTF-8') {
6405 4
            $max_length = (int) \min(
6406 4
                \mb_strlen($str1, $encoding),
6407 4
                \mb_strlen($str2, $encoding)
6408
            );
6409
6410 4
            $longest_common_suffix = '';
6411 4
            for ($i = 1; $i <= $max_length; ++$i) {
6412 4
                $char = \mb_substr($str1, -$i, 1);
6413
6414
                if (
6415 4
                    $char !== false
6416
                    &&
6417 4
                    $char === \mb_substr($str2, -$i, 1)
6418
                ) {
6419 3
                    $longest_common_suffix = $char . $longest_common_suffix;
6420
                } else {
6421 3
                    break;
6422
                }
6423
            }
6424
        } else {
6425 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6426
6427 4
            $max_length = (int) \min(
6428 4
                self::strlen($str1, $encoding),
6429 4
                self::strlen($str2, $encoding)
6430
            );
6431
6432 4
            $longest_common_suffix = '';
6433 4
            for ($i = 1; $i <= $max_length; ++$i) {
6434 4
                $char = self::substr($str1, -$i, 1, $encoding);
6435
6436
                if (
6437 4
                    $char !== false
6438
                    &&
6439 4
                    $char === self::substr($str2, -$i, 1, $encoding)
6440
                ) {
6441 3
                    $longest_common_suffix = $char . $longest_common_suffix;
6442
                } else {
6443 3
                    break;
6444
                }
6445
            }
6446
        }
6447
6448 8
        return $longest_common_suffix;
6449
    }
6450
6451
    /**
6452
     * Returns true if $str matches the supplied pattern, false otherwise.
6453
     *
6454
     * @param string $str     <p>The input string.</p>
6455
     * @param string $pattern <p>Regex pattern to match against.</p>
6456
     *
6457
     * @return bool whether or not $str matches the pattern
6458
     */
6459
    public static function str_matches_pattern(string $str, string $pattern): bool
6460
    {
6461
        return (bool) \preg_match('/' . $pattern . '/u', $str);
6462
    }
6463
6464
    /**
6465
     * Returns whether or not a character exists at an index. Offsets may be
6466
     * negative to count from the last character in the string. Implements
6467
     * part of the ArrayAccess interface.
6468
     *
6469
     * @param string $str      <p>The input string.</p>
6470
     * @param int    $offset   <p>The index to check.</p>
6471
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6472
     *
6473
     * @return bool whether or not the index exists
6474
     */
6475
    public static function str_offset_exists(string $str, int $offset, string $encoding = 'UTF-8'): bool
6476
    {
6477
        // init
6478 6
        $length = (int) self::strlen($str, $encoding);
6479
6480 6
        if ($offset >= 0) {
6481 3
            return $length > $offset;
6482
        }
6483
6484 3
        return $length >= \abs($offset);
6485
    }
6486
6487
    /**
6488
     * Returns the character at the given index. Offsets may be negative to
6489
     * count from the last character in the string. Implements part of the
6490
     * ArrayAccess interface, and throws an OutOfBoundsException if the index
6491
     * does not exist.
6492
     *
6493
     * @param string $str      <p>The input string.</p>
6494
     * @param int    $index    <p>The <strong>index</strong> from which to retrieve the char.</p>
6495
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6496
     *
6497
     * @throws \OutOfBoundsException if the positive or negative offset does not exist
6498
     *
6499
     * @return string the character at the specified index
6500
     */
6501
    public static function str_offset_get(string $str, int $index, string $encoding = 'UTF-8'): string
6502
    {
6503
        // init
6504 2
        $length = (int) self::strlen($str);
6505
6506
        if (
6507 2
            ($index >= 0 && $length <= $index)
6508
            ||
6509 2
            $length < \abs($index)
6510
        ) {
6511 1
            throw new \OutOfBoundsException('No character exists at the index');
6512
        }
6513
6514 1
        return self::char_at($str, $index, $encoding);
6515
    }
6516
6517
    /**
6518
     * Pad a UTF-8 string to a given length with another string.
6519
     *
6520
     * @param string     $str        <p>The input string.</p>
6521
     * @param int        $pad_length <p>The length of return string.</p>
6522
     * @param string     $pad_string [optional] <p>String to use for padding the input string.</p>
6523
     * @param int|string $pad_type   [optional] <p>
6524
     *                               Can be <strong>STR_PAD_RIGHT</strong> (default), [or string "right"]<br>
6525
     *                               <strong>STR_PAD_LEFT</strong> [or string "left"] or<br>
6526
     *                               <strong>STR_PAD_BOTH</strong> [or string "both"]
6527
     *                               </p>
6528
     * @param string     $encoding   [optional] <p>Default: 'UTF-8'</p>
6529
     *
6530
     * @return string returns the padded string
6531
     */
6532
    public static function str_pad(
6533
        string $str,
6534
        int $pad_length,
6535
        string $pad_string = ' ',
6536
        $pad_type = \STR_PAD_RIGHT,
6537
        string $encoding = 'UTF-8'
6538
    ): string {
6539 41
        if ($pad_length === 0 || $pad_string === '') {
6540 1
            return $str;
6541
        }
6542
6543 41
        if ($pad_type !== (int) $pad_type) {
6544 13
            if ($pad_type === 'left') {
6545 3
                $pad_type = \STR_PAD_LEFT;
6546 10
            } elseif ($pad_type === 'right') {
6547 6
                $pad_type = \STR_PAD_RIGHT;
6548 4
            } elseif ($pad_type === 'both') {
6549 3
                $pad_type = \STR_PAD_BOTH;
6550
            } else {
6551 1
                throw new \InvalidArgumentException(
6552 1
                    'Pad expects $pad_type to be "STR_PAD_*" or ' . "to be one of 'left', 'right' or 'both'"
6553
                );
6554
            }
6555
        }
6556
6557 40
        if ($encoding === 'UTF-8') {
6558 25
            $str_length = (int) \mb_strlen($str);
6559
6560 25
            if ($pad_length >= $str_length) {
6561
                switch ($pad_type) {
6562 25
                    case \STR_PAD_LEFT:
6563 8
                        $ps_length = (int) \mb_strlen($pad_string);
6564
6565 8
                        $diff = ($pad_length - $str_length);
6566
6567 8
                        $pre = (string) \mb_substr(
6568 8
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6569 8
                            0,
6570 8
                            $diff
6571
                        );
6572 8
                        $post = '';
6573
6574 8
                        break;
6575
6576 20
                    case \STR_PAD_BOTH:
6577 14
                        $diff = ($pad_length - $str_length);
6578
6579 14
                        $ps_length_left = (int) \floor($diff / 2);
6580
6581 14
                        $ps_length_right = (int) \ceil($diff / 2);
6582
6583 14
                        $pre = (string) \mb_substr(
6584 14
                            \str_repeat($pad_string, $ps_length_left),
6585 14
                            0,
6586 14
                            $ps_length_left
6587
                        );
6588 14
                        $post = (string) \mb_substr(
6589 14
                            \str_repeat($pad_string, $ps_length_right),
6590 14
                            0,
6591 14
                            $ps_length_right
6592
                        );
6593
6594 14
                        break;
6595
6596 9
                    case \STR_PAD_RIGHT:
6597
                    default:
6598 9
                        $ps_length = (int) \mb_strlen($pad_string);
6599
6600 9
                        $diff = ($pad_length - $str_length);
6601
6602 9
                        $post = (string) \mb_substr(
6603 9
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6604 9
                            0,
6605 9
                            $diff
6606
                        );
6607 9
                        $pre = '';
6608
                }
6609
6610 25
                return $pre . $str . $post;
6611
            }
6612
6613 3
            return $str;
6614
        }
6615
6616 15
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6617
6618 15
        $str_length = (int) self::strlen($str, $encoding);
6619
6620 15
        if ($pad_length >= $str_length) {
6621
            switch ($pad_type) {
6622 14
                case \STR_PAD_LEFT:
6623 5
                    $ps_length = (int) self::strlen($pad_string, $encoding);
6624
6625 5
                    $diff = ($pad_length - $str_length);
6626
6627 5
                    $pre = (string) self::substr(
6628 5
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6629 5
                        0,
6630 5
                        $diff,
6631 5
                        $encoding
6632
                    );
6633 5
                    $post = '';
6634
6635 5
                    break;
6636
6637 9
                case \STR_PAD_BOTH:
6638 3
                    $diff = ($pad_length - $str_length);
6639
6640 3
                    $ps_length_left = (int) \floor($diff / 2);
6641
6642 3
                    $ps_length_right = (int) \ceil($diff / 2);
6643
6644 3
                    $pre = (string) self::substr(
6645 3
                        \str_repeat($pad_string, $ps_length_left),
6646 3
                        0,
6647 3
                        $ps_length_left,
6648 3
                        $encoding
6649
                    );
6650 3
                    $post = (string) self::substr(
6651 3
                        \str_repeat($pad_string, $ps_length_right),
6652 3
                        0,
6653 3
                        $ps_length_right,
6654 3
                        $encoding
6655
                    );
6656
6657 3
                    break;
6658
6659 6
                case \STR_PAD_RIGHT:
6660
                default:
6661 6
                    $ps_length = (int) self::strlen($pad_string, $encoding);
6662
6663 6
                    $diff = ($pad_length - $str_length);
6664
6665 6
                    $post = (string) self::substr(
6666 6
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
6667 6
                        0,
6668 6
                        $diff,
6669 6
                        $encoding
6670
                    );
6671 6
                    $pre = '';
6672
            }
6673
6674 14
            return $pre . $str . $post;
6675
        }
6676
6677 1
        return $str;
6678
    }
6679
6680
    /**
6681
     * Returns a new string of a given length such that both sides of the
6682
     * string are padded. Alias for "UTF8::str_pad()" with a $pad_type of 'both'.
6683
     *
6684
     * @param string $str
6685
     * @param int    $length   <p>Desired string length after padding.</p>
6686
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
6687
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6688
     *
6689
     * @return string
6690
     *                <p>The string with padding applied.</p>
6691
     */
6692
    public static function str_pad_both(
6693
        string $str,
6694
        int $length,
6695
        string $pad_str = ' ',
6696
        string $encoding = 'UTF-8'
6697
    ): string {
6698 11
        return self::str_pad(
6699 11
            $str,
6700 11
            $length,
6701 11
            $pad_str,
6702 11
            \STR_PAD_BOTH,
6703 11
            $encoding
6704
        );
6705
    }
6706
6707
    /**
6708
     * Returns a new string of a given length such that the beginning of the
6709
     * string is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'left'.
6710
     *
6711
     * @param string $str
6712
     * @param int    $length   <p>Desired string length after padding.</p>
6713
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
6714
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6715
     *
6716
     * @return string
6717
     *                <p>The string with left padding.</p>
6718
     */
6719
    public static function str_pad_left(
6720
        string $str,
6721
        int $length,
6722
        string $pad_str = ' ',
6723
        string $encoding = 'UTF-8'
6724
    ): string {
6725 7
        return self::str_pad(
6726 7
            $str,
6727 7
            $length,
6728 7
            $pad_str,
6729 7
            \STR_PAD_LEFT,
6730 7
            $encoding
6731
        );
6732
    }
6733
6734
    /**
6735
     * Returns a new string of a given length such that the end of the string
6736
     * is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'right'.
6737
     *
6738
     * @param string $str
6739
     * @param int    $length   <p>Desired string length after padding.</p>
6740
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
6741
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6742
     *
6743
     * @return string
6744
     *                <p>The string with right padding.</p>
6745
     */
6746
    public static function str_pad_right(
6747
        string $str,
6748
        int $length,
6749
        string $pad_str = ' ',
6750
        string $encoding = 'UTF-8'
6751
    ): string {
6752 7
        return self::str_pad(
6753 7
            $str,
6754 7
            $length,
6755 7
            $pad_str,
6756 7
            \STR_PAD_RIGHT,
6757 7
            $encoding
6758
        );
6759
    }
6760
6761
    /**
6762
     * Repeat a string.
6763
     *
6764
     * @param string $str        <p>
6765
     *                           The string to be repeated.
6766
     *                           </p>
6767
     * @param int    $multiplier <p>
6768
     *                           Number of time the input string should be
6769
     *                           repeated.
6770
     *                           </p>
6771
     *                           <p>
6772
     *                           multiplier has to be greater than or equal to 0.
6773
     *                           If the multiplier is set to 0, the function
6774
     *                           will return an empty string.
6775
     *                           </p>
6776
     *
6777
     * @return string
6778
     *                <p>The repeated string.</P>
6779
     */
6780
    public static function str_repeat(string $str, int $multiplier): string
6781
    {
6782 9
        $str = self::filter($str);
6783
6784 9
        return \str_repeat($str, $multiplier);
6785
    }
6786
6787
    /**
6788
     * INFO: This is only a wrapper for "str_replace()"  -> the original functions is already UTF-8 safe.
6789
     *
6790
     * Replace all occurrences of the search string with the replacement string
6791
     *
6792
     * @see http://php.net/manual/en/function.str-replace.php
6793
     *
6794
     * @param mixed $search  <p>
6795
     *                       The value being searched for, otherwise known as the needle.
6796
     *                       An array may be used to designate multiple needles.
6797
     *                       </p>
6798
     * @param mixed $replace <p>
6799
     *                       The replacement value that replaces found search
6800
     *                       values. An array may be used to designate multiple replacements.
6801
     *                       </p>
6802
     * @param mixed $subject <p>
6803
     *                       The string or array being searched and replaced on,
6804
     *                       otherwise known as the haystack.
6805
     *                       </p>
6806
     *                       <p>
6807
     *                       If subject is an array, then the search and
6808
     *                       replace is performed with every entry of
6809
     *                       subject, and the return value is an array as
6810
     *                       well.
6811
     *                       </p>
6812
     * @param int   $count   [optional] If passed, this will hold the number of matched and replaced needles
6813
     *
6814
     * @return mixed this function returns a string or an array with the replaced values
6815
     */
6816
    public static function str_replace(
6817
        $search,
6818
        $replace,
6819
        $subject,
6820
        int &$count = null
6821
    ) {
6822
        /**
6823
         * @psalm-suppress PossiblyNullArgument
6824
         */
6825 12
        return \str_replace(
6826 12
            $search,
6827 12
            $replace,
6828 12
            $subject,
6829 12
            $count
6830
        );
6831
    }
6832
6833
    /**
6834
     * Replaces $search from the beginning of string with $replacement.
6835
     *
6836
     * @param string $str         <p>The input string.</p>
6837
     * @param string $search      <p>The string to search for.</p>
6838
     * @param string $replacement <p>The replacement.</p>
6839
     *
6840
     * @return string string after the replacements
6841
     */
6842
    public static function str_replace_beginning(
6843
        string $str,
6844
        string $search,
6845
        string $replacement
6846
    ): string {
6847 17
        if ($str === '') {
6848 4
            if ($replacement === '') {
6849 2
                return '';
6850
            }
6851
6852 2
            if ($search === '') {
6853 2
                return $replacement;
6854
            }
6855
        }
6856
6857 13
        if ($search === '') {
6858 2
            return $str . $replacement;
6859
        }
6860
6861 11
        if (\strpos($str, $search) === 0) {
6862 9
            return $replacement . \substr($str, \strlen($search));
6863
        }
6864
6865 2
        return $str;
6866
    }
6867
6868
    /**
6869
     * Replaces $search from the ending of string with $replacement.
6870
     *
6871
     * @param string $str         <p>The input string.</p>
6872
     * @param string $search      <p>The string to search for.</p>
6873
     * @param string $replacement <p>The replacement.</p>
6874
     *
6875
     * @return string string after the replacements
6876
     */
6877
    public static function str_replace_ending(
6878
        string $str,
6879
        string $search,
6880
        string $replacement
6881
    ): string {
6882 17
        if ($str === '') {
6883 4
            if ($replacement === '') {
6884 2
                return '';
6885
            }
6886
6887 2
            if ($search === '') {
6888 2
                return $replacement;
6889
            }
6890
        }
6891
6892 13
        if ($search === '') {
6893 2
            return $str . $replacement;
6894
        }
6895
6896 11
        if (\strpos($str, $search, \strlen($str) - \strlen($search)) !== false) {
6897 8
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
6898
        }
6899
6900 11
        return $str;
6901
    }
6902
6903
    /**
6904
     * Replace the first "$search"-term with the "$replace"-term.
6905
     *
6906
     * @param string $search
6907
     * @param string $replace
6908
     * @param string $subject
6909
     *
6910
     * @return string
6911
     *
6912
     * @psalm-suppress InvalidReturnType
6913
     */
6914
    public static function str_replace_first(
6915
        string $search,
6916
        string $replace,
6917
        string $subject
6918
    ): string {
6919 2
        $pos = self::strpos($subject, $search);
6920
6921 2
        if ($pos !== false) {
6922
            /**
6923
             * @psalm-suppress InvalidReturnStatement
6924
             */
6925 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...
6926 2
                $subject,
6927 2
                $replace,
6928 2
                $pos,
6929 2
                (int) self::strlen($search)
6930
            );
6931
        }
6932
6933 2
        return $subject;
6934
    }
6935
6936
    /**
6937
     * Replace the last "$search"-term with the "$replace"-term.
6938
     *
6939
     * @param string $search
6940
     * @param string $replace
6941
     * @param string $subject
6942
     *
6943
     * @return string
6944
     *
6945
     * @psalm-suppress InvalidReturnType
6946
     */
6947
    public static function str_replace_last(
6948
        string $search,
6949
        string $replace,
6950
        string $subject
6951
    ): string {
6952 2
        $pos = self::strrpos($subject, $search);
6953 2
        if ($pos !== false) {
6954
            /**
6955
             * @psalm-suppress InvalidReturnStatement
6956
             */
6957 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...
6958 2
                $subject,
6959 2
                $replace,
6960 2
                $pos,
6961 2
                (int) self::strlen($search)
6962
            );
6963
        }
6964
6965 2
        return $subject;
6966
    }
6967
6968
    /**
6969
     * Shuffles all the characters in the string.
6970
     *
6971
     * PS: uses random algorithm which is weak for cryptography purposes
6972
     *
6973
     * @param string $str      <p>The input string</p>
6974
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6975
     *
6976
     * @return string the shuffled string
6977
     */
6978
    public static function str_shuffle(string $str, string $encoding = 'UTF-8'): string
6979
    {
6980 5
        if ($encoding === 'UTF-8') {
6981 5
            $indexes = \range(0, (int) \mb_strlen($str) - 1);
6982
            /** @noinspection NonSecureShuffleUsageInspection */
6983 5
            \shuffle($indexes);
6984
6985
            // init
6986 5
            $shuffled_str = '';
6987
6988 5
            foreach ($indexes as &$i) {
6989 5
                $tmp_sub_str = \mb_substr($str, $i, 1);
6990 5
                if ($tmp_sub_str !== false) {
6991 5
                    $shuffled_str .= $tmp_sub_str;
6992
                }
6993
            }
6994
        } else {
6995
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6996
6997
            $indexes = \range(0, (int) self::strlen($str, $encoding) - 1);
6998
            /** @noinspection NonSecureShuffleUsageInspection */
6999
            \shuffle($indexes);
7000
7001
            // init
7002
            $shuffled_str = '';
7003
7004
            foreach ($indexes as &$i) {
7005
                $tmp_sub_str = self::substr($str, $i, 1, $encoding);
7006
                if ($tmp_sub_str !== false) {
7007
                    $shuffled_str .= $tmp_sub_str;
7008
                }
7009
            }
7010
        }
7011
7012 5
        return $shuffled_str;
7013
    }
7014
7015
    /**
7016
     * Returns the substring beginning at $start, and up to, but not including
7017
     * the index specified by $end. If $end is omitted, the function extracts
7018
     * the remaining string. If $end is negative, it is computed from the end
7019
     * of the string.
7020
     *
7021
     * @param string $str
7022
     * @param int    $start    <p>Initial index from which to begin extraction.</p>
7023
     * @param int    $end      [optional] <p>Index at which to end extraction. Default: null</p>
7024
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7025
     *
7026
     * @return false|string
7027
     *                      <p>The extracted substring.</p><p>If <i>str</i> is shorter than <i>start</i>
7028
     *                      characters long, <b>FALSE</b> will be returned.
7029
     */
7030
    public static function str_slice(
7031
        string $str,
7032
        int $start,
7033
        int $end = null,
7034
        string $encoding = 'UTF-8'
7035
    ) {
7036 18
        if ($encoding === 'UTF-8') {
7037 7
            if ($end === null) {
7038 1
                $length = (int) \mb_strlen($str);
7039 6
            } elseif ($end >= 0 && $end <= $start) {
7040 2
                return '';
7041 4
            } elseif ($end < 0) {
7042 1
                $length = (int) \mb_strlen($str) + $end - $start;
7043
            } else {
7044 3
                $length = $end - $start;
7045
            }
7046
7047 5
            return \mb_substr($str, $start, $length);
7048
        }
7049
7050 11
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7051
7052 11
        if ($end === null) {
7053 5
            $length = (int) self::strlen($str, $encoding);
7054 6
        } elseif ($end >= 0 && $end <= $start) {
7055 2
            return '';
7056 4
        } elseif ($end < 0) {
7057 1
            $length = (int) self::strlen($str, $encoding) + $end - $start;
7058
        } else {
7059 3
            $length = $end - $start;
7060
        }
7061
7062 9
        return self::substr($str, $start, $length, $encoding);
7063
    }
7064
7065
    /**
7066
     * Convert a string to e.g.: "snake_case"
7067
     *
7068
     * @param string $str
7069
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7070
     *
7071
     * @return string string in snake_case
7072
     */
7073
    public static function str_snakeize(string $str, string $encoding = 'UTF-8'): string
7074
    {
7075 22
        if ($str === '') {
7076
            return '';
7077
        }
7078
7079 22
        $str = \str_replace(
7080 22
            '-',
7081 22
            '_',
7082 22
            self::normalize_whitespace($str)
7083
        );
7084
7085 22
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
7086 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7087
        }
7088
7089 22
        $str = (string) \preg_replace_callback(
7090 22
            '/([\\p{N}|\\p{Lu}])/u',
7091
            /**
7092
             * @param string[] $matches
7093
             *
7094
             * @return string
7095
             */
7096
            static function (array $matches) use ($encoding): string {
7097 9
                $match = $matches[1];
7098 9
                $match_int = (int) $match;
7099
7100 9
                if ((string) $match_int === $match) {
7101 4
                    return '_' . $match . '_';
7102
                }
7103
7104 5
                if ($encoding === 'UTF-8') {
7105 5
                    return '_' . \mb_strtolower($match);
7106
                }
7107
7108
                return '_' . self::strtolower($match, $encoding);
7109 22
            },
7110 22
            $str
7111
        );
7112
7113 22
        $str = (string) \preg_replace(
7114
            [
7115 22
                '/\\s+/u',           // convert spaces to "_"
7116
                '/^\\s+|\\s+$/u', // trim leading & trailing spaces
7117
                '/_+/',                 // remove double "_"
7118
            ],
7119
            [
7120 22
                '_',
7121
                '',
7122
                '_',
7123
            ],
7124 22
            $str
7125
        );
7126
7127 22
        return \trim(\trim($str, '_')); // trim leading & trailing "_" + whitespace
7128
    }
7129
7130
    /**
7131
     * Sort all characters according to code points.
7132
     *
7133
     * @param string $str    <p>A UTF-8 string.</p>
7134
     * @param bool   $unique <p>Sort unique. If <strong>true</strong>, repeated characters are ignored.</p>
7135
     * @param bool   $desc   <p>If <strong>true</strong>, will sort characters in reverse code point order.</p>
7136
     *
7137
     * @return string string of sorted characters
7138
     */
7139
    public static function str_sort(string $str, bool $unique = false, bool $desc = false): string
7140
    {
7141 2
        $array = self::codepoints($str);
7142
7143 2
        if ($unique) {
7144 2
            $array = \array_flip(\array_flip($array));
7145
        }
7146
7147 2
        if ($desc) {
7148 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

7148
            \arsort(/** @scrutinizer ignore-type */ $array);
Loading history...
7149
        } else {
7150 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

7150
            \asort(/** @scrutinizer ignore-type */ $array);
Loading history...
7151
        }
7152
7153 2
        return self::string($array);
7154
    }
7155
7156
    /**
7157
     * Convert a string to an array of Unicode characters.
7158
     *
7159
     * @param int|int[]|string|string[] $str                     <p>The string to split into array.</p>
7160
     * @param int                       $length                  [optional] <p>Max character length of each array
7161
     *                                                           element.</p>
7162
     * @param bool                      $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
7163
     * @param bool                      $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
7164
     *                                                           "mb_substr"</p>
7165
     *
7166
     * @return array
7167
     *               <p>An array containing chunks of the input.</p>
7168
     */
7169
    public static function str_split(
7170
        $str,
7171
        int $length = 1,
7172
        bool $clean_utf8 = false,
7173
        bool $try_to_use_mb_functions = true
7174
    ): array {
7175 89
        if ($length <= 0) {
7176 3
            return [];
7177
        }
7178
7179 88
        if (\is_array($str) === true) {
7180 2
            foreach ($str as $k => &$v) {
7181 2
                $v = self::str_split(
7182 2
                    $v,
7183 2
                    $length,
7184 2
                    $clean_utf8,
7185 2
                    $try_to_use_mb_functions
7186
                );
7187
            }
7188
7189 2
            return $str;
7190
        }
7191
7192
        // init
7193 88
        $str = (string) $str;
7194
7195 88
        if ($str === '') {
7196 13
            return [];
7197
        }
7198
7199 85
        if ($clean_utf8 === true) {
7200 19
            $str = self::clean($str);
7201
        }
7202
7203
        if (
7204 85
            $try_to_use_mb_functions === true
7205
            &&
7206 85
            self::$SUPPORT['mbstring'] === true
7207
        ) {
7208 81
            $i_max = \mb_strlen($str);
7209 81
            if ($i_max <= 127) {
7210 75
                $ret = [];
7211 75
                for ($i = 0; $i < $i_max; ++$i) {
7212 75
                    $ret[] = \mb_substr($str, $i, 1);
7213
                }
7214
            } else {
7215 16
                $return_array = [];
7216 16
                \preg_match_all('/./us', $str, $return_array);
7217 81
                $ret = $return_array[0] ?? [];
7218
            }
7219 23
        } elseif (self::$SUPPORT['pcre_utf8'] === true) {
7220 17
            $return_array = [];
7221 17
            \preg_match_all('/./us', $str, $return_array);
7222 17
            $ret = $return_array[0] ?? [];
7223
        } else {
7224
7225
            // fallback
7226
7227 8
            $ret = [];
7228 8
            $len = \strlen($str);
7229
7230
            /** @noinspection ForeachInvariantsInspection */
7231 8
            for ($i = 0; $i < $len; ++$i) {
7232 8
                if (($str[$i] & "\x80") === "\x00") {
7233 8
                    $ret[] = $str[$i];
7234
                } elseif (
7235 8
                    isset($str[$i + 1])
7236
                    &&
7237 8
                    ($str[$i] & "\xE0") === "\xC0"
7238
                ) {
7239 4
                    if (($str[$i + 1] & "\xC0") === "\x80") {
7240 4
                        $ret[] = $str[$i] . $str[$i + 1];
7241
7242 4
                        ++$i;
7243
                    }
7244
                } elseif (
7245 6
                    isset($str[$i + 2])
7246
                    &&
7247 6
                    ($str[$i] & "\xF0") === "\xE0"
7248
                ) {
7249
                    if (
7250 6
                        ($str[$i + 1] & "\xC0") === "\x80"
7251
                        &&
7252 6
                        ($str[$i + 2] & "\xC0") === "\x80"
7253
                    ) {
7254 6
                        $ret[] = $str[$i] . $str[$i + 1] . $str[$i + 2];
7255
7256 6
                        $i += 2;
7257
                    }
7258
                } elseif (
7259
                    isset($str[$i + 3])
7260
                    &&
7261
                    ($str[$i] & "\xF8") === "\xF0"
7262
                ) {
7263
                    if (
7264
                        ($str[$i + 1] & "\xC0") === "\x80"
7265
                        &&
7266
                        ($str[$i + 2] & "\xC0") === "\x80"
7267
                        &&
7268
                        ($str[$i + 3] & "\xC0") === "\x80"
7269
                    ) {
7270
                        $ret[] = $str[$i] . $str[$i + 1] . $str[$i + 2] . $str[$i + 3];
7271
7272
                        $i += 3;
7273
                    }
7274
                }
7275
            }
7276
        }
7277
7278 85
        if ($length > 1) {
7279 11
            $ret = \array_chunk($ret, $length);
7280
7281 11
            return \array_map(
7282
                static function (array &$item): string {
7283 11
                    return \implode('', $item);
7284 11
                },
7285 11
                $ret
7286
            );
7287
        }
7288
7289 78
        if (isset($ret[0]) && $ret[0] === '') {
7290
            return [];
7291
        }
7292
7293 78
        return $ret;
7294
    }
7295
7296
    /**
7297
     * Splits the string with the provided regular expression, returning an
7298
     * array of strings. An optional integer $limit will truncate the
7299
     * results.
7300
     *
7301
     * @param string $str
7302
     * @param string $pattern <p>The regex with which to split the string.</p>
7303
     * @param int    $limit   [optional] <p>Maximum number of results to return. Default: -1 === no limit</p>
7304
     *
7305
     * @return string[] an array of strings
7306
     */
7307
    public static function str_split_pattern(string $str, string $pattern, int $limit = -1): array
7308
    {
7309 16
        if ($limit === 0) {
7310 2
            return [];
7311
        }
7312
7313 14
        if ($pattern === '') {
7314 1
            return [$str];
7315
        }
7316
7317 13
        if (self::$SUPPORT['mbstring'] === true) {
7318 13
            if ($limit >= 0) {
7319
                /** @noinspection PhpComposerExtensionStubsInspection */
7320 8
                $result_tmp = \mb_split($pattern, $str);
7321
7322 8
                $result = [];
7323 8
                foreach ($result_tmp as $item_tmp) {
7324 8
                    if ($limit === 0) {
7325 4
                        break;
7326
                    }
7327 8
                    --$limit;
7328
7329 8
                    $result[] = $item_tmp;
7330
                }
7331
7332 8
                return $result;
7333
            }
7334
7335
            /** @noinspection PhpComposerExtensionStubsInspection */
7336 5
            return \mb_split($pattern, $str);
7337
        }
7338
7339
        if ($limit > 0) {
7340
            ++$limit;
7341
        } else {
7342
            $limit = -1;
7343
        }
7344
7345
        $array = \preg_split('/' . \preg_quote($pattern, '/') . '/u', $str, $limit);
7346
7347
        if ($array === false) {
7348
            return [];
7349
        }
7350
7351
        if ($limit > 0 && \count($array) === $limit) {
7352
            \array_pop($array);
7353
        }
7354
7355
        return $array;
7356
    }
7357
7358
    /**
7359
     * Check if the string starts with the given substring.
7360
     *
7361
     * @param string $haystack <p>The string to search in.</p>
7362
     * @param string $needle   <p>The substring to search for.</p>
7363
     *
7364
     * @return bool
7365
     */
7366
    public static function str_starts_with(string $haystack, string $needle): bool
7367
    {
7368 19
        if ($needle === '') {
7369 2
            return true;
7370
        }
7371
7372 19
        if ($haystack === '') {
7373
            return false;
7374
        }
7375
7376 19
        return \strpos($haystack, $needle) === 0;
7377
    }
7378
7379
    /**
7380
     * Returns true if the string begins with any of $substrings, false otherwise.
7381
     *
7382
     * - case-sensitive
7383
     *
7384
     * @param string $str        <p>The input string.</p>
7385
     * @param array  $substrings <p>Substrings to look for.</p>
7386
     *
7387
     * @return bool whether or not $str starts with $substring
7388
     */
7389
    public static function str_starts_with_any(string $str, array $substrings): bool
7390
    {
7391 8
        if ($str === '') {
7392
            return false;
7393
        }
7394
7395 8
        if ($substrings === []) {
7396
            return false;
7397
        }
7398
7399 8
        foreach ($substrings as &$substring) {
7400 8
            if (self::str_starts_with($str, $substring)) {
7401 8
                return true;
7402
            }
7403
        }
7404
7405 6
        return false;
7406
    }
7407
7408
    /**
7409
     * Gets the substring after the first occurrence of a separator.
7410
     *
7411
     * @param string $str       <p>The input string.</p>
7412
     * @param string $separator <p>The string separator.</p>
7413
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7414
     *
7415
     * @return string
7416
     */
7417
    public static function str_substr_after_first_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
7418
    {
7419 1
        if ($separator === '' || $str === '') {
7420 1
            return '';
7421
        }
7422
7423 1
        if ($encoding === 'UTF-8') {
7424 1
            $offset = \mb_strpos($str, $separator);
7425 1
            if ($offset === false) {
7426 1
                return '';
7427
            }
7428
7429 1
            return (string) \mb_substr(
7430 1
                $str,
7431 1
                $offset + (int) \mb_strlen($separator)
7432
            );
7433
        }
7434
7435
        $offset = self::strpos($str, $separator, 0, $encoding);
7436
        if ($offset === false) {
7437
            return '';
7438
        }
7439
7440
        return (string) \mb_substr(
7441
            $str,
7442
            $offset + (int) self::strlen($separator, $encoding),
7443
            null,
7444
            $encoding
7445
        );
7446
    }
7447
7448
    /**
7449
     * Gets the substring after the last occurrence of a separator.
7450
     *
7451
     * @param string $str       <p>The input string.</p>
7452
     * @param string $separator <p>The string separator.</p>
7453
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7454
     *
7455
     * @return string
7456
     */
7457
    public static function str_substr_after_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
7458
    {
7459 1
        if ($separator === '' || $str === '') {
7460 1
            return '';
7461
        }
7462
7463 1
        if ($encoding === 'UTF-8') {
7464 1
            $offset = \mb_strrpos($str, $separator);
7465 1
            if ($offset === false) {
7466 1
                return '';
7467
            }
7468
7469 1
            return (string) \mb_substr(
7470 1
                $str,
7471 1
                $offset + (int) \mb_strlen($separator)
7472
            );
7473
        }
7474
7475
        $offset = self::strrpos($str, $separator, 0, $encoding);
7476
        if ($offset === false) {
7477
            return '';
7478
        }
7479
7480
        return (string) self::substr(
7481
            $str,
7482
            $offset + (int) self::strlen($separator, $encoding),
7483
            null,
7484
            $encoding
7485
        );
7486
    }
7487
7488
    /**
7489
     * Gets the substring before the first occurrence of a separator.
7490
     *
7491
     * @param string $str       <p>The input string.</p>
7492
     * @param string $separator <p>The string separator.</p>
7493
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7494
     *
7495
     * @return string
7496
     */
7497
    public static function str_substr_before_first_separator(
7498
        string $str,
7499
        string $separator,
7500
        string $encoding = 'UTF-8'
7501
    ): string {
7502 1
        if ($separator === '' || $str === '') {
7503 1
            return '';
7504
        }
7505
7506 1
        if ($encoding === 'UTF-8') {
7507 1
            $offset = \mb_strpos($str, $separator);
7508 1
            if ($offset === false) {
7509 1
                return '';
7510
            }
7511
7512 1
            return (string) \mb_substr(
7513 1
                $str,
7514 1
                0,
7515 1
                $offset
7516
            );
7517
        }
7518
7519
        $offset = self::strpos($str, $separator, 0, $encoding);
7520
        if ($offset === false) {
7521
            return '';
7522
        }
7523
7524
        return (string) self::substr(
7525
            $str,
7526
            0,
7527
            $offset,
7528
            $encoding
7529
        );
7530
    }
7531
7532
    /**
7533
     * Gets the substring before the last occurrence of a separator.
7534
     *
7535
     * @param string $str       <p>The input string.</p>
7536
     * @param string $separator <p>The string separator.</p>
7537
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
7538
     *
7539
     * @return string
7540
     */
7541
    public static function str_substr_before_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
7542
    {
7543 1
        if ($separator === '' || $str === '') {
7544 1
            return '';
7545
        }
7546
7547 1
        if ($encoding === 'UTF-8') {
7548 1
            $offset = \mb_strrpos($str, $separator);
7549 1
            if ($offset === false) {
7550 1
                return '';
7551
            }
7552
7553 1
            return (string) \mb_substr(
7554 1
                $str,
7555 1
                0,
7556 1
                $offset
7557
            );
7558
        }
7559
7560
        $offset = self::strrpos($str, $separator, 0, $encoding);
7561
        if ($offset === false) {
7562
            return '';
7563
        }
7564
7565
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7566
7567
        return (string) self::substr(
7568
            $str,
7569
            0,
7570
            $offset,
7571
            $encoding
7572
        );
7573
    }
7574
7575
    /**
7576
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
7577
     *
7578
     * @param string $str           <p>The input string.</p>
7579
     * @param string $needle        <p>The string to look for.</p>
7580
     * @param bool   $before_needle [optional] <p>Default: false</p>
7581
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
7582
     *
7583
     * @return string
7584
     */
7585
    public static function str_substr_first(
7586
        string $str,
7587
        string $needle,
7588
        bool $before_needle = false,
7589
        string $encoding = 'UTF-8'
7590
    ): string {
7591 2
        if ($str === '' || $needle === '') {
7592 2
            return '';
7593
        }
7594
7595 2
        if ($encoding === 'UTF-8') {
7596 2
            if ($before_needle === true) {
7597 1
                $part = \mb_strstr(
7598 1
                    $str,
7599 1
                    $needle,
7600 1
                    $before_needle
7601
                );
7602
            } else {
7603 1
                $part = \mb_strstr(
7604 1
                    $str,
7605 2
                    $needle
7606
                );
7607
            }
7608
        } else {
7609
            $part = self::strstr(
7610
                $str,
7611
                $needle,
7612
                $before_needle,
7613
                $encoding
7614
            );
7615
        }
7616
7617 2
        return $part === false ? '' : $part;
7618
    }
7619
7620
    /**
7621
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
7622
     *
7623
     * @param string $str           <p>The input string.</p>
7624
     * @param string $needle        <p>The string to look for.</p>
7625
     * @param bool   $before_needle [optional] <p>Default: false</p>
7626
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
7627
     *
7628
     * @return string
7629
     */
7630
    public static function str_substr_last(
7631
        string $str,
7632
        string $needle,
7633
        bool $before_needle = false,
7634
        string $encoding = 'UTF-8'
7635
    ): string {
7636 2
        if ($str === '' || $needle === '') {
7637 2
            return '';
7638
        }
7639
7640 2
        if ($encoding === 'UTF-8') {
7641 2
            if ($before_needle === true) {
7642 1
                $part = \mb_strrchr(
7643 1
                    $str,
7644 1
                    $needle,
7645 1
                    $before_needle
7646
                );
7647
            } else {
7648 1
                $part = \mb_strrchr(
7649 1
                    $str,
7650 2
                    $needle
7651
                );
7652
            }
7653
        } else {
7654
            $part = self::strrchr(
7655
                $str,
7656
                $needle,
7657
                $before_needle,
7658
                $encoding
7659
            );
7660
        }
7661
7662 2
        return $part === false ? '' : $part;
7663
    }
7664
7665
    /**
7666
     * Surrounds $str with the given substring.
7667
     *
7668
     * @param string $str
7669
     * @param string $substring <p>The substring to add to both sides.</P>
7670
     *
7671
     * @return string string with the substring both prepended and appended
7672
     */
7673
    public static function str_surround(string $str, string $substring): string
7674
    {
7675 5
        return $substring . $str . $substring;
7676
    }
7677
7678
    /**
7679
     * Returns a trimmed string with the first letter of each word capitalized.
7680
     * Also accepts an array, $ignore, allowing you to list words not to be
7681
     * capitalized.
7682
     *
7683
     * @param string              $str
7684
     * @param array|string[]|null $ignore                        [optional] <p>An array of words not to capitalize or null.
7685
     *                                                           Default: null</p>
7686
     * @param string              $encoding                      [optional] <p>Default: 'UTF-8'</p>
7687
     * @param bool                $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
7688
     * @param string|null         $lang                          [optional] <p>Set the language for special cases: az, el, lt,
7689
     *                                                           tr</p>
7690
     * @param bool                $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
7691
     *                                                           ß</p>
7692
     * @param bool                $use_trim_first                [optional] <p>true === trim the input string, first</p>
7693
     * @param string|null         $word_define_chars             [optional] <p>An string of chars that will be used as whitespace separator === words.</p>
7694
     *
7695
     * @return string
7696
     *                <p>The titleized string.</p>
7697
     */
7698
    public static function str_titleize(
7699
        string $str,
7700
        array $ignore = null,
7701
        string $encoding = 'UTF-8',
7702
        bool $clean_utf8 = false,
7703
        string $lang = null,
7704
        bool $try_to_keep_the_string_length = false,
7705
        bool $use_trim_first = true,
7706
        string $word_define_chars = null
7707
    ): string {
7708 10
        if ($str === '') {
7709
            return '';
7710
        }
7711
7712 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
7713 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7714
        }
7715
7716 10
        if ($use_trim_first === true) {
7717 10
            $str = \trim($str);
7718
        }
7719
7720 10
        if ($clean_utf8 === true) {
7721
            $str = self::clean($str);
7722
        }
7723
7724 10
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
7725
7726 10
        if ($word_define_chars) {
7727 4
            $word_define_chars = \preg_quote($word_define_chars, '/');
7728
        } else {
7729 6
            $word_define_chars = '';
7730
        }
7731
7732 10
        $str = (string) \preg_replace_callback(
7733 10
            '/([^\\s' . $word_define_chars . ']+)/u',
7734
            static function (array $match) use ($try_to_keep_the_string_length, $lang, $ignore, $use_mb_functions, $encoding): string {
7735 10
                if ($ignore !== null && \in_array($match[0], $ignore, true)) {
7736 4
                    return $match[0];
7737
                }
7738
7739 10
                if ($use_mb_functions === true) {
7740 10
                    if ($encoding === 'UTF-8') {
7741 10
                        return \mb_strtoupper(\mb_substr($match[0], 0, 1))
7742 10
                               . \mb_strtolower(\mb_substr($match[0], 1));
7743
                    }
7744
7745
                    return \mb_strtoupper(\mb_substr($match[0], 0, 1, $encoding), $encoding)
7746
                           . \mb_strtolower(\mb_substr($match[0], 1, null, $encoding), $encoding);
7747
                }
7748
7749
                return self::ucfirst(
7750
                    self::strtolower(
7751
                        $match[0],
7752
                        $encoding,
7753
                        false,
7754
                        $lang,
7755
                        $try_to_keep_the_string_length
7756
                    ),
7757
                    $encoding,
7758
                    false,
7759
                    $lang,
7760
                    $try_to_keep_the_string_length
7761
                );
7762 10
            },
7763 10
            $str
7764
        );
7765
7766 10
        return $str;
7767
    }
7768
7769
    /**
7770
     * Returns a trimmed string in proper title case.
7771
     *
7772
     * Also accepts an array, $ignore, allowing you to list words not to be
7773
     * capitalized.
7774
     *
7775
     * Adapted from John Gruber's script.
7776
     *
7777
     * @see https://gist.github.com/gruber/9f9e8650d68b13ce4d78
7778
     *
7779
     * @param string $str
7780
     * @param array  $ignore   <p>An array of words not to capitalize.</p>
7781
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7782
     *
7783
     * @return string the titleized string
7784
     */
7785
    public static function str_titleize_for_humans(
7786
        string $str,
7787
        array $ignore = [],
7788
        string $encoding = 'UTF-8'
7789
    ): string {
7790 35
        $small_words = \array_merge(
7791
            [
7792 35
                '(?<!q&)a',
7793
                'an',
7794
                'and',
7795
                'as',
7796
                'at(?!&t)',
7797
                'but',
7798
                'by',
7799
                'en',
7800
                'for',
7801
                'if',
7802
                'in',
7803
                'of',
7804
                'on',
7805
                'or',
7806
                'the',
7807
                'to',
7808
                'v[.]?',
7809
                'via',
7810
                'vs[.]?',
7811
            ],
7812 35
            $ignore
7813
        );
7814
7815 35
        $small_words_rx = \implode('|', $small_words);
7816 35
        $apostrophe_rx = '(?x: [\'’] [[:lower:]]* )?';
7817
7818 35
        $str = \trim($str);
7819
7820 35
        if (self::has_lowercase($str) === false) {
7821 2
            $str = self::strtolower($str, $encoding);
7822
        }
7823
7824
        // the main substitutions
7825 35
        $str = (string) \preg_replace_callback(
7826
            '~\\b (_*) (?:                                                         # 1. Leading underscore and
7827
                        ( (?<=[ ][/\\\\]) [[:alpha:]]+ [-_[:alpha:]/\\\\]+ |              # 2. file path or 
7828 35
                          [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ' . $apostrophe_rx . ' ) #    URL, domain, or email
7829
                        |
7830 35
                        ( (?i: ' . $small_words_rx . ' ) ' . $apostrophe_rx . ' )            # 3. or small word (case-insensitive)
7831
                        |
7832 35
                        ( [[:alpha:]] [[:lower:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 4. or word w/o internal caps
7833
                        |
7834 35
                        ( [[:alpha:]] [[:alpha:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 5. or some other word
7835
                      ) (_*) \\b                                                          # 6. With trailing underscore
7836
                    ~ux',
7837
            /**
7838
             * @param string[] $matches
7839
             *
7840
             * @return string
7841
             */
7842
            static function (array $matches) use ($encoding): string {
7843
                // preserve leading underscore
7844 35
                $str = $matches[1];
7845 35
                if ($matches[2]) {
7846
                    // preserve URLs, domains, emails and file paths
7847 5
                    $str .= $matches[2];
7848 35
                } elseif ($matches[3]) {
7849
                    // lower-case small words
7850 25
                    $str .= self::strtolower($matches[3], $encoding);
7851 35
                } elseif ($matches[4]) {
7852
                    // capitalize word w/o internal caps
7853 34
                    $str .= static::ucfirst($matches[4], $encoding);
7854
                } else {
7855
                    // preserve other kinds of word (iPhone)
7856 7
                    $str .= $matches[5];
7857
                }
7858
                // preserve trailing underscore
7859 35
                $str .= $matches[6];
7860
7861 35
                return $str;
7862 35
            },
7863 35
            $str
7864
        );
7865
7866
        // Exceptions for small words: capitalize at start of title...
7867 35
        $str = (string) \preg_replace_callback(
7868
            '~(  \\A [[:punct:]]*            # start of title...
7869
                      |  [:.;?!][ ]+                # or of subsentence...
7870
                      |  [ ][\'"“‘(\[][ ]* )        # or of inserted subphrase...
7871 35
                      ( ' . $small_words_rx . ' ) \\b # ...followed by small word
7872
                     ~uxi',
7873
            /**
7874
             * @param string[] $matches
7875
             *
7876
             * @return string
7877
             */
7878
            static function (array $matches) use ($encoding): string {
7879 11
                return $matches[1] . static::ucfirst($matches[2], $encoding);
7880 35
            },
7881 35
            $str
7882
        );
7883
7884
        // ...and end of title
7885 35
        $str = (string) \preg_replace_callback(
7886 35
            '~\\b ( ' . $small_words_rx . ' ) # small word...
7887
                      (?= [[:punct:]]* \Z          # ...at the end of the title...
7888
                      |   [\'"’”)\]] [ ] )         # ...or of an inserted subphrase?
7889
                     ~uxi',
7890
            /**
7891
             * @param string[] $matches
7892
             *
7893
             * @return string
7894
             */
7895
            static function (array $matches) use ($encoding): string {
7896 3
                return static::ucfirst($matches[1], $encoding);
7897 35
            },
7898 35
            $str
7899
        );
7900
7901
        // Exceptions for small words in hyphenated compound words.
7902
        // e.g. "in-flight" -> In-Flight
7903 35
        $str = (string) \preg_replace_callback(
7904
            '~\\b
7905
                        (?<! -)                   # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (in-flight)
7906 35
                        ( ' . $small_words_rx . ' )
7907
                        (?= -[[:alpha:]]+)        # lookahead for "-someword"
7908
                       ~uxi',
7909
            /**
7910
             * @param string[] $matches
7911
             *
7912
             * @return string
7913
             */
7914
            static function (array $matches) use ($encoding): string {
7915
                return static::ucfirst($matches[1], $encoding);
7916 35
            },
7917 35
            $str
7918
        );
7919
7920
        // e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
7921 35
        $str = (string) \preg_replace_callback(
7922
            '~\\b
7923
                      (?<!…)                    # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (stand-in)
7924
                      ( [[:alpha:]]+- )         # $1 = first word and hyphen, should already be properly capped
7925 35
                      ( ' . $small_words_rx . ' ) # ...followed by small word
7926
                      (?!	- )                 # Negative lookahead for another -
7927
                     ~uxi',
7928
            /**
7929
             * @param string[] $matches
7930
             *
7931
             * @return string
7932
             */
7933
            static function (array $matches) use ($encoding): string {
7934
                return $matches[1] . static::ucfirst($matches[2], $encoding);
7935 35
            },
7936 35
            $str
7937
        );
7938
7939 35
        return $str;
7940
    }
7941
7942
    /**
7943
     * Get a binary representation of a specific string.
7944
     *
7945
     * @param string $str <p>The input string.</p>
7946
     *
7947
     * @return false|string
7948
     *                      <p>false on error</p>
7949
     */
7950
    public static function str_to_binary(string $str)
7951
    {
7952
        /** @var array|false $value - needed for PhpStan (stubs error) */
7953 2
        $value = \unpack('H*', $str);
7954 2
        if ($value === false) {
7955
            return false;
7956
        }
7957
7958
        /** @noinspection OffsetOperationsInspection */
7959 2
        return \base_convert($value[1], 16, 2);
7960
    }
7961
7962
    /**
7963
     * @param string   $str
7964
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
7965
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
7966
     *
7967
     * @return string[]
7968
     */
7969
    public static function str_to_lines(string $str, bool $remove_empty_values = false, int $remove_short_values = null): array
7970
    {
7971 17
        if ($str === '') {
7972 1
            return $remove_empty_values === true ? [] : [''];
7973
        }
7974
7975 16
        if (self::$SUPPORT['mbstring'] === true) {
7976
            /** @noinspection PhpComposerExtensionStubsInspection */
7977 16
            $return = \mb_split("[\r\n]{1,2}", $str);
7978
        } else {
7979
            $return = \preg_split("/[\r\n]{1,2}/u", $str);
7980
        }
7981
7982 16
        if ($return === false) {
7983
            return $remove_empty_values === true ? [] : [''];
7984
        }
7985
7986
        if (
7987 16
            $remove_short_values === null
7988
            &&
7989 16
            $remove_empty_values === false
7990
        ) {
7991 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...
7992
        }
7993
7994
        return self::reduce_string_array(
7995
            $return,
7996
            $remove_empty_values,
7997
            $remove_short_values
7998
        );
7999
    }
8000
8001
    /**
8002
     * Convert a string into an array of words.
8003
     *
8004
     * @param string   $str
8005
     * @param string   $char_list           <p>Additional chars for the definition of "words".</p>
8006
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
8007
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
8008
     *
8009
     * @return string[]
8010
     */
8011
    public static function str_to_words(
8012
        string $str,
8013
        string $char_list = '',
8014
        bool $remove_empty_values = false,
8015
        int $remove_short_values = null
8016
    ): array {
8017 13
        if ($str === '') {
8018 4
            return $remove_empty_values === true ? [] : [''];
8019
        }
8020
8021 13
        $char_list = self::rxClass($char_list, '\pL');
8022
8023 13
        $return = \preg_split("/({$char_list}+(?:[\p{Pd}’']{$char_list}+)*)/u", $str, -1, \PREG_SPLIT_DELIM_CAPTURE);
8024 13
        if ($return === false) {
8025
            return $remove_empty_values === true ? [] : [''];
8026
        }
8027
8028
        if (
8029 13
            $remove_short_values === null
8030
            &&
8031 13
            $remove_empty_values === false
8032
        ) {
8033 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...
8034
        }
8035
8036 2
        $tmp_return = self::reduce_string_array(
8037 2
            $return,
8038 2
            $remove_empty_values,
8039 2
            $remove_short_values
8040
        );
8041
8042 2
        foreach ($tmp_return as &$item) {
8043 2
            $item = (string) $item;
8044
        }
8045
8046 2
        return $tmp_return;
8047
    }
8048
8049
    /**
8050
     * alias for "UTF8::to_ascii()"
8051
     *
8052
     * @param string $str
8053
     * @param string $unknown
8054
     * @param bool   $strict
8055
     *
8056
     * @return string
8057
     *
8058
     * @see UTF8::to_ascii()
8059
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
8060
     */
8061
    public static function str_transliterate(
8062
        string $str,
8063
        string $unknown = '?',
8064
        bool $strict = false
8065
    ): string {
8066 7
        return self::to_ascii($str, $unknown, $strict);
8067
    }
8068
8069
    /**
8070
     * Truncates the string to a given length. If $substring is provided, and
8071
     * truncating occurs, the string is further truncated so that the substring
8072
     * may be appended without exceeding the desired length.
8073
     *
8074
     * @param string $str
8075
     * @param int    $length    <p>Desired length of the truncated string.</p>
8076
     * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
8077
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8078
     *
8079
     * @return string string after truncating
8080
     */
8081
    public static function str_truncate(
8082
        string $str,
8083
        int $length,
8084
        string $substring = '',
8085
        string $encoding = 'UTF-8'
8086
    ): string {
8087 22
        if ($str === '') {
8088
            return '';
8089
        }
8090
8091 22
        if ($encoding === 'UTF-8') {
8092 10
            if ($length >= (int) \mb_strlen($str)) {
8093 2
                return $str;
8094
            }
8095
8096 8
            if ($substring !== '') {
8097 4
                $length -= (int) \mb_strlen($substring);
8098
8099
                /** @noinspection UnnecessaryCastingInspection */
8100 4
                return (string) \mb_substr($str, 0, $length) . $substring;
8101
            }
8102
8103
            /** @noinspection UnnecessaryCastingInspection */
8104 4
            return (string) \mb_substr($str, 0, $length);
8105
        }
8106
8107 12
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8108
8109 12
        if ($length >= (int) self::strlen($str, $encoding)) {
8110 2
            return $str;
8111
        }
8112
8113 10
        if ($substring !== '') {
8114 6
            $length -= (int) self::strlen($substring, $encoding);
8115
        }
8116
8117
        return (
8118 10
               (string) self::substr(
8119 10
                   $str,
8120 10
                   0,
8121 10
                   $length,
8122 10
                   $encoding
8123
               )
8124 10
               ) . $substring;
8125
    }
8126
8127
    /**
8128
     * Truncates the string to a given length, while ensuring that it does not
8129
     * split words. If $substring is provided, and truncating occurs, the
8130
     * string is further truncated so that the substring may be appended without
8131
     * exceeding the desired length.
8132
     *
8133
     * @param string $str
8134
     * @param int    $length                                 <p>Desired length of the truncated string.</p>
8135
     * @param string $substring                              [optional] <p>The substring to append if it can fit. Default:
8136
     *                                                       ''</p>
8137
     * @param string $encoding                               [optional] <p>Default: 'UTF-8'</p>
8138
     * @param bool   $ignore_do_not_split_words_for_one_word [optional] <p>Default: false</p>
8139
     *
8140
     * @return string string after truncating
8141
     */
8142
    public static function str_truncate_safe(
8143
        string $str,
8144
        int $length,
8145
        string $substring = '',
8146
        string $encoding = 'UTF-8',
8147
        bool $ignore_do_not_split_words_for_one_word = false
8148
    ): string {
8149 47
        if ($str === '' || $length <= 0) {
8150 1
            return $substring;
8151
        }
8152
8153 47
        if ($encoding === 'UTF-8') {
8154 21
            if ($length >= (int) \mb_strlen($str)) {
8155 5
                return $str;
8156
            }
8157
8158
            // need to further trim the string so we can append the substring
8159 17
            $length -= (int) \mb_strlen($substring);
8160 17
            if ($length <= 0) {
8161 1
                return $substring;
8162
            }
8163
8164
            /** @var false|string $truncated - needed for PhpStan (stubs error) */
8165 17
            $truncated = \mb_substr($str, 0, $length);
8166 17
            if ($truncated === false) {
8167
                return '';
8168
            }
8169
8170
            // if the last word was truncated
8171 17
            $space_position = \mb_strpos($str, ' ', $length - 1);
8172 17
            if ($space_position !== $length) {
8173
                // find pos of the last occurrence of a space, get up to that
8174 13
                $last_position = \mb_strrpos($truncated, ' ', 0);
8175
8176
                if (
8177 13
                    $last_position !== false
8178
                    ||
8179 13
                    ($space_position !== false && $ignore_do_not_split_words_for_one_word === false)
8180
                ) {
8181 17
                    $truncated = (string) \mb_substr($truncated, 0, (int) $last_position);
8182
                }
8183
            }
8184
        } else {
8185 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8186
8187 26
            if ($length >= (int) self::strlen($str, $encoding)) {
8188 4
                return $str;
8189
            }
8190
8191
            // need to further trim the string so we can append the substring
8192 22
            $length -= (int) self::strlen($substring, $encoding);
8193 22
            if ($length <= 0) {
8194
                return $substring;
8195
            }
8196
8197 22
            $truncated = self::substr($str, 0, $length, $encoding);
8198
8199 22
            if ($truncated === false) {
8200
                return '';
8201
            }
8202
8203
            // if the last word was truncated
8204 22
            $space_position = self::strpos($str, ' ', $length - 1, $encoding);
8205 22
            if ($space_position !== $length) {
8206
                // find pos of the last occurrence of a space, get up to that
8207 12
                $last_position = self::strrpos($truncated, ' ', 0, $encoding);
8208
8209
                if (
8210 12
                    $last_position !== false
8211
                    ||
8212 12
                    ($space_position !== false && $ignore_do_not_split_words_for_one_word === false)
8213
                ) {
8214 9
                    $truncated = (string) self::substr($truncated, 0, (int) $last_position, $encoding);
8215
                }
8216
            }
8217
        }
8218
8219 39
        return $truncated . $substring;
8220
    }
8221
8222
    /**
8223
     * Returns a lowercase and trimmed string separated by underscores.
8224
     * Underscores are inserted before uppercase characters (with the exception
8225
     * of the first character of the string), and in place of spaces as well as
8226
     * dashes.
8227
     *
8228
     * @param string $str
8229
     *
8230
     * @return string the underscored string
8231
     */
8232
    public static function str_underscored(string $str): string
8233
    {
8234 16
        return self::str_delimit($str, '_');
8235
    }
8236
8237
    /**
8238
     * Returns an UpperCamelCase version of the supplied string. It trims
8239
     * surrounding spaces, capitalizes letters following digits, spaces, dashes
8240
     * and underscores, and removes spaces, dashes, underscores.
8241
     *
8242
     * @param string      $str                           <p>The input string.</p>
8243
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
8244
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
8245
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
8246
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
8247
     *
8248
     * @return string string in UpperCamelCase
8249
     */
8250
    public static function str_upper_camelize(
8251
        string $str,
8252
        string $encoding = 'UTF-8',
8253
        bool $clean_utf8 = false,
8254
        string $lang = null,
8255
        bool $try_to_keep_the_string_length = false
8256
    ): string {
8257 13
        return self::ucfirst(self::str_camelize($str, $encoding), $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
8258
    }
8259
8260
    /**
8261
     * alias for "UTF8::ucfirst()"
8262
     *
8263
     * @param string      $str
8264
     * @param string      $encoding
8265
     * @param bool        $clean_utf8
8266
     * @param string|null $lang
8267
     * @param bool        $try_to_keep_the_string_length
8268
     *
8269
     * @return string
8270
     *
8271
     * @see UTF8::ucfirst()
8272
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
8273
     */
8274
    public static function str_upper_first(
8275
        string $str,
8276
        string $encoding = 'UTF-8',
8277
        bool $clean_utf8 = false,
8278
        string $lang = null,
8279
        bool $try_to_keep_the_string_length = false
8280
    ): string {
8281 5
        return self::ucfirst(
8282 5
            $str,
8283 5
            $encoding,
8284 5
            $clean_utf8,
8285 5
            $lang,
8286 5
            $try_to_keep_the_string_length
8287
        );
8288
    }
8289
8290
    /**
8291
     * Get the number of words in a specific string.
8292
     *
8293
     * @param string $str       <p>The input string.</p>
8294
     * @param int    $format    [optional] <p>
8295
     *                          <strong>0</strong> => return a number of words (default)<br>
8296
     *                          <strong>1</strong> => return an array of words<br>
8297
     *                          <strong>2</strong> => return an array of words with word-offset as key
8298
     *                          </p>
8299
     * @param string $char_list [optional] <p>Additional chars that contains to words and do not start a new word.</p>
8300
     *
8301
     * @return int|string[] The number of words in the string
8302
     */
8303
    public static function str_word_count(string $str, int $format = 0, string $char_list = '')
8304
    {
8305 2
        $str_parts = self::str_to_words($str, $char_list);
8306
8307 2
        $len = \count($str_parts);
8308
8309 2
        if ($format === 1) {
8310 2
            $number_of_words = [];
8311 2
            for ($i = 1; $i < $len; $i += 2) {
8312 2
                $number_of_words[] = $str_parts[$i];
8313
            }
8314 2
        } elseif ($format === 2) {
8315 2
            $number_of_words = [];
8316 2
            $offset = (int) self::strlen($str_parts[0]);
8317 2
            for ($i = 1; $i < $len; $i += 2) {
8318 2
                $number_of_words[$offset] = $str_parts[$i];
8319 2
                $offset += (int) self::strlen($str_parts[$i]) + (int) self::strlen($str_parts[$i + 1]);
8320
            }
8321
        } else {
8322 2
            $number_of_words = (int) (($len - 1) / 2);
8323
        }
8324
8325 2
        return $number_of_words;
8326
    }
8327
8328
    /**
8329
     * Case-insensitive string comparison.
8330
     *
8331
     * INFO: Case-insensitive version of UTF8::strcmp()
8332
     *
8333
     * @param string $str1     <p>The first string.</p>
8334
     * @param string $str2     <p>The second string.</p>
8335
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8336
     *
8337
     * @return int
8338
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
8339
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
8340
     *             <strong>0</strong> if they are equal
8341
     */
8342
    public static function strcasecmp(
8343
        string $str1,
8344
        string $str2,
8345
        string $encoding = 'UTF-8'
8346
    ): int {
8347 23
        return self::strcmp(
8348 23
            self::strtocasefold(
8349 23
                $str1,
8350 23
                true,
8351 23
                false,
8352 23
                $encoding,
8353 23
                null,
8354 23
                false
8355
            ),
8356 23
            self::strtocasefold(
8357 23
                $str2,
8358 23
                true,
8359 23
                false,
8360 23
                $encoding,
8361 23
                null,
8362 23
                false
8363
            )
8364
        );
8365
    }
8366
8367
    /**
8368
     * alias for "UTF8::strstr()"
8369
     *
8370
     * @param string $haystack
8371
     * @param string $needle
8372
     * @param bool   $before_needle
8373
     * @param string $encoding
8374
     * @param bool   $clean_utf8
8375
     *
8376
     * @return false|string
8377
     *
8378
     * @see UTF8::strstr()
8379
     * @deprecated <p>please use "UTF8::strstr()"</p>
8380
     */
8381
    public static function strchr(
8382
        string $haystack,
8383
        string $needle,
8384
        bool $before_needle = false,
8385
        string $encoding = 'UTF-8',
8386
        bool $clean_utf8 = false
8387
    ) {
8388 2
        return self::strstr(
8389 2
            $haystack,
8390 2
            $needle,
8391 2
            $before_needle,
8392 2
            $encoding,
8393 2
            $clean_utf8
8394
        );
8395
    }
8396
8397
    /**
8398
     * Case-sensitive string comparison.
8399
     *
8400
     * @param string $str1 <p>The first string.</p>
8401
     * @param string $str2 <p>The second string.</p>
8402
     *
8403
     * @return int
8404
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
8405
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
8406
     *             <strong>0</strong> if they are equal
8407
     */
8408
    public static function strcmp(string $str1, string $str2): int
8409
    {
8410 29
        if ($str1 === $str2) {
8411 21
            return 0;
8412
        }
8413
8414 24
        return \strcmp(
8415 24
            \Normalizer::normalize($str1, \Normalizer::NFD),
8416 24
            \Normalizer::normalize($str2, \Normalizer::NFD)
8417
        );
8418
    }
8419
8420
    /**
8421
     * Find length of initial segment not matching mask.
8422
     *
8423
     * @param string $str
8424
     * @param string $char_list
8425
     * @param int    $offset
8426
     * @param int    $length
8427
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
8428
     *
8429
     * @return int
8430
     */
8431
    public static function strcspn(
8432
        string $str,
8433
        string $char_list,
8434
        int $offset = null,
8435
        int $length = null,
8436
        string $encoding = 'UTF-8'
8437
    ): int {
8438 12
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
8439
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8440
        }
8441
8442 12
        if ($char_list === '') {
8443 2
            return (int) self::strlen($str, $encoding);
8444
        }
8445
8446 11
        if ($offset !== null || $length !== null) {
8447 3
            if ($encoding === 'UTF-8') {
8448 3
                if ($length === null) {
8449
                    /** @noinspection UnnecessaryCastingInspection */
8450 2
                    $str_tmp = \mb_substr($str, (int) $offset);
8451
                } else {
8452
                    /** @noinspection UnnecessaryCastingInspection */
8453 3
                    $str_tmp = \mb_substr($str, (int) $offset, $length);
8454
                }
8455
            } else {
8456
                /** @noinspection UnnecessaryCastingInspection */
8457
                $str_tmp = self::substr($str, (int) $offset, $length, $encoding);
8458
            }
8459
8460 3
            if ($str_tmp === false) {
8461
                return 0;
8462
            }
8463
8464
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
8465 3
            $str = $str_tmp;
8466
        }
8467
8468 11
        if ($str === '') {
8469 2
            return 0;
8470
        }
8471
8472 10
        $matches = [];
8473 10
        if (\preg_match('/^(.*?)' . self::rxClass($char_list) . '/us', $str, $matches)) {
8474 9
            $return = self::strlen($matches[1], $encoding);
8475 9
            if ($return === false) {
8476
                return 0;
8477
            }
8478
8479 9
            return $return;
8480
        }
8481
8482 2
        return (int) self::strlen($str, $encoding);
8483
    }
8484
8485
    /**
8486
     * alias for "UTF8::stristr()"
8487
     *
8488
     * @param string $haystack
8489
     * @param string $needle
8490
     * @param bool   $before_needle
8491
     * @param string $encoding
8492
     * @param bool   $clean_utf8
8493
     *
8494
     * @return false|string
8495
     *
8496
     * @see UTF8::stristr()
8497
     * @deprecated <p>please use "UTF8::stristr()"</p>
8498
     */
8499
    public static function strichr(
8500
        string $haystack,
8501
        string $needle,
8502
        bool $before_needle = false,
8503
        string $encoding = 'UTF-8',
8504
        bool $clean_utf8 = false
8505
    ) {
8506 1
        return self::stristr(
8507 1
            $haystack,
8508 1
            $needle,
8509 1
            $before_needle,
8510 1
            $encoding,
8511 1
            $clean_utf8
8512
        );
8513
    }
8514
8515
    /**
8516
     * Create a UTF-8 string from code points.
8517
     *
8518
     * INFO: opposite to UTF8::codepoints()
8519
     *
8520
     * @param array $array <p>Integer or Hexadecimal codepoints.</p>
8521
     *
8522
     * @return string UTF-8 encoded string
8523
     */
8524
    public static function string(array $array): string
8525
    {
8526 4
        return \implode(
8527 4
            '',
8528 4
            \array_map(
8529
                [
8530 4
                    self::class,
8531
                    'chr',
8532
                ],
8533 4
                $array
8534
            )
8535
        );
8536
    }
8537
8538
    /**
8539
     * Checks if string starts with "BOM" (Byte Order Mark Character) character.
8540
     *
8541
     * @param string $str <p>The input string.</p>
8542
     *
8543
     * @return bool
8544
     *              <strong>true</strong> if the string has BOM at the start,<br>
8545
     *              <strong>false</strong> otherwise
8546
     */
8547
    public static function string_has_bom(string $str): bool
8548
    {
8549
        /** @noinspection PhpUnusedLocalVariableInspection */
8550 6
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
8551 6
            if (\strpos($str, $bom_string) === 0) {
8552 6
                return true;
8553
            }
8554
        }
8555
8556 6
        return false;
8557
    }
8558
8559
    /**
8560
     * Strip HTML and PHP tags from a string + clean invalid UTF-8.
8561
     *
8562
     * @see http://php.net/manual/en/function.strip-tags.php
8563
     *
8564
     * @param string $str            <p>
8565
     *                               The input string.
8566
     *                               </p>
8567
     * @param string $allowable_tags [optional] <p>
8568
     *                               You can use the optional second parameter to specify tags which should
8569
     *                               not be stripped.
8570
     *                               </p>
8571
     *                               <p>
8572
     *                               HTML comments and PHP tags are also stripped. This is hardcoded and
8573
     *                               can not be changed with allowable_tags.
8574
     *                               </p>
8575
     * @param bool   $clean_utf8     [optional] <p>Remove non UTF-8 chars from the string.</p>
8576
     *
8577
     * @return string
8578
     *                <p>The stripped string.</p>
8579
     */
8580
    public static function strip_tags(
8581
        string $str,
8582
        string $allowable_tags = null,
8583
        bool $clean_utf8 = false
8584
    ): string {
8585 4
        if ($str === '') {
8586 1
            return '';
8587
        }
8588
8589 4
        if ($clean_utf8 === true) {
8590 2
            $str = self::clean($str);
8591
        }
8592
8593 4
        if ($allowable_tags === null) {
8594 4
            return \strip_tags($str);
8595
        }
8596
8597 2
        return \strip_tags($str, $allowable_tags);
8598
    }
8599
8600
    /**
8601
     * Strip all whitespace characters. This includes tabs and newline
8602
     * characters, as well as multibyte whitespace such as the thin space
8603
     * and ideographic space.
8604
     *
8605
     * @param string $str
8606
     *
8607
     * @return string
8608
     */
8609
    public static function strip_whitespace(string $str): string
8610
    {
8611 36
        if ($str === '') {
8612 3
            return '';
8613
        }
8614
8615 33
        return (string) \preg_replace('/[[:space:]]+/u', '', $str);
8616
    }
8617
8618
    /**
8619
     * Find the position of the first occurrence of a substring in a string, case-insensitive.
8620
     *
8621
     * @see http://php.net/manual/en/function.mb-stripos.php
8622
     *
8623
     * @param string $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
8624
     * @param string $needle     <p>The string to find in haystack.</p>
8625
     * @param int    $offset     [optional] <p>The position in haystack to start searching.</p>
8626
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
8627
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
8628
     *
8629
     * @return false|int
8630
     *                   Return the <strong>(int)</strong> numeric position of the first occurrence of needle in the
8631
     *                   haystack string,<br> or <strong>false</strong> if needle is not found
8632
     */
8633
    public static function stripos(
8634
        string $haystack,
8635
        string $needle,
8636
        int $offset = 0,
8637
        $encoding = 'UTF-8',
8638
        bool $clean_utf8 = false
8639
    ) {
8640 24
        if ($haystack === '' || $needle === '') {
8641 5
            return false;
8642
        }
8643
8644 23
        if ($clean_utf8 === true) {
8645
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
8646
            // if invalid characters are found in $haystack before $needle
8647 1
            $haystack = self::clean($haystack);
8648 1
            $needle = self::clean($needle);
8649
        }
8650
8651 23
        if (self::$SUPPORT['mbstring'] === true) {
8652 23
            if ($encoding === 'UTF-8') {
8653 23
                return \mb_stripos($haystack, $needle, $offset);
8654
            }
8655
8656 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8657
8658 3
            return \mb_stripos($haystack, $needle, $offset, $encoding);
8659
        }
8660
8661 2
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8662
8663
        if (
8664 2
            $encoding === 'UTF-8' // INFO: "grapheme_stripos()" can't handle other encodings
8665
            &&
8666 2
            $offset >= 0 // grapheme_stripos() can't handle negative offset
8667
            &&
8668 2
            self::$SUPPORT['intl'] === true
8669
        ) {
8670
            $return_tmp = \grapheme_stripos($haystack, $needle, $offset);
8671
            if ($return_tmp !== false) {
8672
                return $return_tmp;
8673
            }
8674
        }
8675
8676
        //
8677
        // fallback for ascii only
8678
        //
8679
8680 2
        if (ASCII::is_ascii($haystack . $needle)) {
8681
            return \stripos($haystack, $needle, $offset);
8682
        }
8683
8684
        //
8685
        // fallback via vanilla php
8686
        //
8687
8688 2
        $haystack = self::strtocasefold($haystack, true, false, $encoding, null, false);
8689 2
        $needle = self::strtocasefold($needle, true, false, $encoding, null, false);
8690
8691 2
        return self::strpos($haystack, $needle, $offset, $encoding);
8692
    }
8693
8694
    /**
8695
     * Returns all of haystack starting from and including the first occurrence of needle to the end.
8696
     *
8697
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
8698
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
8699
     * @param bool   $before_needle [optional] <p>
8700
     *                              If <b>TRUE</b>, it returns the part of the
8701
     *                              haystack before the first occurrence of the needle (excluding the needle).
8702
     *                              </p>
8703
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
8704
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
8705
     *
8706
     * @return false|string
8707
     *                      <p>A sub-string,<br>or <strong>false</strong> if needle is not found.</p>
8708
     */
8709
    public static function stristr(
8710
        string $haystack,
8711
        string $needle,
8712
        bool $before_needle = false,
8713
        string $encoding = 'UTF-8',
8714
        bool $clean_utf8 = false
8715
    ) {
8716 12
        if ($haystack === '' || $needle === '') {
8717 3
            return false;
8718
        }
8719
8720 9
        if ($clean_utf8 === true) {
8721
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
8722
            // if invalid characters are found in $haystack before $needle
8723 1
            $needle = self::clean($needle);
8724 1
            $haystack = self::clean($haystack);
8725
        }
8726
8727 9
        if (!$needle) {
8728
            return $haystack;
8729
        }
8730
8731 9
        if (self::$SUPPORT['mbstring'] === true) {
8732 9
            if ($encoding === 'UTF-8') {
8733 9
                return \mb_stristr($haystack, $needle, $before_needle);
8734
            }
8735
8736 1
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8737
8738 1
            return \mb_stristr($haystack, $needle, $before_needle, $encoding);
8739
        }
8740
8741
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8742
8743
        if (
8744
            $encoding !== 'UTF-8'
8745
            &&
8746
            self::$SUPPORT['mbstring'] === false
8747
        ) {
8748
            \trigger_error('UTF8::stristr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
8749
        }
8750
8751
        if (
8752
            $encoding === 'UTF-8' // INFO: "grapheme_stristr()" can't handle other encodings
8753
            &&
8754
            self::$SUPPORT['intl'] === true
8755
        ) {
8756
            $return_tmp = \grapheme_stristr($haystack, $needle, $before_needle);
8757
            if ($return_tmp !== false) {
8758
                return $return_tmp;
8759
            }
8760
        }
8761
8762
        if (ASCII::is_ascii($needle . $haystack)) {
8763
            return \stristr($haystack, $needle, $before_needle);
8764
        }
8765
8766
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/usi', $haystack, $match);
8767
8768
        if (!isset($match[1])) {
8769
            return false;
8770
        }
8771
8772
        if ($before_needle) {
8773
            return $match[1];
8774
        }
8775
8776
        return self::substr($haystack, (int) self::strlen($match[1], $encoding), null, $encoding);
8777
    }
8778
8779
    /**
8780
     * Get the string length, not the byte-length!
8781
     *
8782
     * @see http://php.net/manual/en/function.mb-strlen.php
8783
     *
8784
     * @param string $str        <p>The string being checked for length.</p>
8785
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
8786
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
8787
     *
8788
     * @return false|int
8789
     *                   <p>
8790
     *                   The number <strong>(int)</strong> of characters in the string $str having character encoding
8791
     *                   $encoding.
8792
     *                   (One multi-byte character counted as +1).
8793
     *                   <br>
8794
     *                   Can return <strong>false</strong>, if e.g. mbstring is not installed and we process invalid
8795
     *                   chars.
8796
     *                   </p>
8797
     */
8798
    public static function strlen(
8799
        string $str,
8800
        string $encoding = 'UTF-8',
8801
        bool $clean_utf8 = false
8802
    ) {
8803 173
        if ($str === '') {
8804 21
            return 0;
8805
        }
8806
8807 171
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
8808 12
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8809
        }
8810
8811 171
        if ($clean_utf8 === true) {
8812
            // "mb_strlen" and "\iconv_strlen" returns wrong length,
8813
            // if invalid characters are found in $str
8814 4
            $str = self::clean($str);
8815
        }
8816
8817
        //
8818
        // fallback via mbstring
8819
        //
8820
8821 171
        if (self::$SUPPORT['mbstring'] === true) {
8822 165
            if ($encoding === 'UTF-8') {
8823 165
                return \mb_strlen($str);
8824
            }
8825
8826 4
            return \mb_strlen($str, $encoding);
8827
        }
8828
8829
        //
8830
        // fallback for binary || ascii only
8831
        //
8832
8833
        if (
8834 8
            $encoding === 'CP850'
8835
            ||
8836 8
            $encoding === 'ASCII'
8837
        ) {
8838
            return \strlen($str);
8839
        }
8840
8841
        if (
8842 8
            $encoding !== 'UTF-8'
8843
            &&
8844 8
            self::$SUPPORT['mbstring'] === false
8845
            &&
8846 8
            self::$SUPPORT['iconv'] === false
8847
        ) {
8848 2
            \trigger_error('UTF8::strlen() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
8849
        }
8850
8851
        //
8852
        // fallback via iconv
8853
        //
8854
8855 8
        if (self::$SUPPORT['iconv'] === true) {
8856
            $return_tmp = \iconv_strlen($str, $encoding);
8857
            if ($return_tmp !== false) {
8858
                return $return_tmp;
8859
            }
8860
        }
8861
8862
        //
8863
        // fallback via intl
8864
        //
8865
8866
        if (
8867 8
            $encoding === 'UTF-8' // INFO: "grapheme_strlen()" can't handle other encodings
8868
            &&
8869 8
            self::$SUPPORT['intl'] === true
8870
        ) {
8871
            $return_tmp = \grapheme_strlen($str);
8872
            if ($return_tmp !== null) {
8873
                return $return_tmp;
8874
            }
8875
        }
8876
8877
        //
8878
        // fallback for ascii only
8879
        //
8880
8881 8
        if (ASCII::is_ascii($str)) {
8882 4
            return \strlen($str);
8883
        }
8884
8885
        //
8886
        // fallback via vanilla php
8887
        //
8888
8889 8
        \preg_match_all('/./us', $str, $parts);
8890
8891 8
        $return_tmp = \count($parts[0]);
8892 8
        if ($return_tmp === 0) {
8893
            return false;
8894
        }
8895
8896 8
        return $return_tmp;
8897
    }
8898
8899
    /**
8900
     * Get string length in byte.
8901
     *
8902
     * @param string $str
8903
     *
8904
     * @return int
8905
     */
8906
    public static function strlen_in_byte(string $str): int
8907
    {
8908
        if ($str === '') {
8909
            return 0;
8910
        }
8911
8912
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
8913
            // "mb_" is available if overload is used, so use it ...
8914
            return \mb_strlen($str, 'CP850'); // 8-BIT
8915
        }
8916
8917
        return \strlen($str);
8918
    }
8919
8920
    /**
8921
     * Case-insensitive string comparisons using a "natural order" algorithm.
8922
     *
8923
     * INFO: natural order version of UTF8::strcasecmp()
8924
     *
8925
     * @param string $str1     <p>The first string.</p>
8926
     * @param string $str2     <p>The second string.</p>
8927
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8928
     *
8929
     * @return int
8930
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
8931
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
8932
     *             <strong>0</strong> if they are equal
8933
     */
8934
    public static function strnatcasecmp(string $str1, string $str2, string $encoding = 'UTF-8'): int
8935
    {
8936 2
        return self::strnatcmp(
8937 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
8938 2
            self::strtocasefold($str2, true, false, $encoding, null, false)
8939
        );
8940
    }
8941
8942
    /**
8943
     * String comparisons using a "natural order" algorithm
8944
     *
8945
     * INFO: natural order version of UTF8::strcmp()
8946
     *
8947
     * @see http://php.net/manual/en/function.strnatcmp.php
8948
     *
8949
     * @param string $str1 <p>The first string.</p>
8950
     * @param string $str2 <p>The second string.</p>
8951
     *
8952
     * @return int
8953
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
8954
     *             <strong>&gt; 0</strong> if str1 is greater than str2;<br>
8955
     *             <strong>0</strong> if they are equal
8956
     */
8957
    public static function strnatcmp(string $str1, string $str2): int
8958
    {
8959 4
        if ($str1 === $str2) {
8960 4
            return 0;
8961
        }
8962
8963 4
        return \strnatcmp(
8964 4
            (string) self::strtonatfold($str1),
8965 4
            (string) self::strtonatfold($str2)
8966
        );
8967
    }
8968
8969
    /**
8970
     * Case-insensitive string comparison of the first n characters.
8971
     *
8972
     * @see http://php.net/manual/en/function.strncasecmp.php
8973
     *
8974
     * @param string $str1     <p>The first string.</p>
8975
     * @param string $str2     <p>The second string.</p>
8976
     * @param int    $len      <p>The length of strings to be used in the comparison.</p>
8977
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8978
     *
8979
     * @return int
8980
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
8981
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
8982
     *             <strong>0</strong> if they are equal
8983
     */
8984
    public static function strncasecmp(
8985
        string $str1,
8986
        string $str2,
8987
        int $len,
8988
        string $encoding = 'UTF-8'
8989
    ): int {
8990 2
        return self::strncmp(
8991 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
8992 2
            self::strtocasefold($str2, true, false, $encoding, null, false),
8993 2
            $len
8994
        );
8995
    }
8996
8997
    /**
8998
     * String comparison of the first n characters.
8999
     *
9000
     * @see http://php.net/manual/en/function.strncmp.php
9001
     *
9002
     * @param string $str1     <p>The first string.</p>
9003
     * @param string $str2     <p>The second string.</p>
9004
     * @param int    $len      <p>Number of characters to use in the comparison.</p>
9005
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9006
     *
9007
     * @return int
9008
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
9009
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
9010
     *             <strong>0</strong> if they are equal
9011
     */
9012
    public static function strncmp(
9013
        string $str1,
9014
        string $str2,
9015
        int $len,
9016
        string $encoding = 'UTF-8'
9017
    ): int {
9018 4
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9019
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9020
        }
9021
9022 4
        if ($encoding === 'UTF-8') {
9023 4
            $str1 = (string) \mb_substr($str1, 0, $len);
9024 4
            $str2 = (string) \mb_substr($str2, 0, $len);
9025
        } else {
9026
            $str1 = (string) self::substr($str1, 0, $len, $encoding);
9027
            $str2 = (string) self::substr($str2, 0, $len, $encoding);
9028
        }
9029
9030 4
        return self::strcmp($str1, $str2);
9031
    }
9032
9033
    /**
9034
     * Search a string for any of a set of characters.
9035
     *
9036
     * @see http://php.net/manual/en/function.strpbrk.php
9037
     *
9038
     * @param string $haystack  <p>The string where char_list is looked for.</p>
9039
     * @param string $char_list <p>This parameter is case-sensitive.</p>
9040
     *
9041
     * @return false|string string starting from the character found, or false if it is not found
9042
     */
9043
    public static function strpbrk(string $haystack, string $char_list)
9044
    {
9045 2
        if ($haystack === '' || $char_list === '') {
9046 2
            return false;
9047
        }
9048
9049 2
        if (\preg_match('/' . self::rxClass($char_list) . '/us', $haystack, $m)) {
9050 2
            return \substr($haystack, (int) \strpos($haystack, $m[0]));
9051
        }
9052
9053 2
        return false;
9054
    }
9055
9056
    /**
9057
     * Find the position of the first occurrence of a substring in a string.
9058
     *
9059
     * @see http://php.net/manual/en/function.mb-strpos.php
9060
     *
9061
     * @param string     $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
9062
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
9063
     * @param int        $offset     [optional] <p>The search offset. If it is not specified, 0 is used.</p>
9064
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9065
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9066
     *
9067
     * @return false|int
9068
     *                   The <strong>(int)</strong> numeric position of the first occurrence of needle in the haystack
9069
     *                   string.<br> If needle is not found it returns false.
9070
     */
9071
    public static function strpos(
9072
        string $haystack,
9073
        $needle,
9074
        int $offset = 0,
9075
        $encoding = 'UTF-8',
9076
        bool $clean_utf8 = false
9077
    ) {
9078 53
        if ($haystack === '') {
9079 4
            return false;
9080
        }
9081
9082
        // iconv and mbstring do not support integer $needle
9083 52
        if ((int) $needle === $needle) {
9084
            $needle = (string) self::chr($needle);
9085
        }
9086 52
        $needle = (string) $needle;
9087
9088 52
        if ($needle === '') {
9089 2
            return false;
9090
        }
9091
9092 52
        if ($clean_utf8 === true) {
9093
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9094
            // if invalid characters are found in $haystack before $needle
9095 3
            $needle = self::clean($needle);
9096 3
            $haystack = self::clean($haystack);
9097
        }
9098
9099 52
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9100 11
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9101
        }
9102
9103
        //
9104
        // fallback via mbstring
9105
        //
9106
9107 52
        if (self::$SUPPORT['mbstring'] === true) {
9108 50
            if ($encoding === 'UTF-8') {
9109 50
                return \mb_strpos($haystack, $needle, $offset);
9110
            }
9111
9112 2
            return \mb_strpos($haystack, $needle, $offset, $encoding);
9113
        }
9114
9115
        //
9116
        // fallback for binary || ascii only
9117
        //
9118
        if (
9119 4
            $encoding === 'CP850'
9120
            ||
9121 4
            $encoding === 'ASCII'
9122
        ) {
9123 2
            return \strpos($haystack, $needle, $offset);
9124
        }
9125
9126
        if (
9127 4
            $encoding !== 'UTF-8'
9128
            &&
9129 4
            self::$SUPPORT['iconv'] === false
9130
            &&
9131 4
            self::$SUPPORT['mbstring'] === false
9132
        ) {
9133 2
            \trigger_error('UTF8::strpos() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9134
        }
9135
9136
        //
9137
        // fallback via intl
9138
        //
9139
9140
        if (
9141 4
            $encoding === 'UTF-8' // INFO: "grapheme_strpos()" can't handle other encodings
9142
            &&
9143 4
            $offset >= 0 // grapheme_strpos() can't handle negative offset
9144
            &&
9145 4
            self::$SUPPORT['intl'] === true
9146
        ) {
9147
            $return_tmp = \grapheme_strpos($haystack, $needle, $offset);
9148
            if ($return_tmp !== false) {
9149
                return $return_tmp;
9150
            }
9151
        }
9152
9153
        //
9154
        // fallback via iconv
9155
        //
9156
9157
        if (
9158 4
            $offset >= 0 // iconv_strpos() can't handle negative offset
9159
            &&
9160 4
            self::$SUPPORT['iconv'] === true
9161
        ) {
9162
            // ignore invalid negative offset to keep compatibility
9163
            // with php < 5.5.35, < 5.6.21, < 7.0.6
9164
            $return_tmp = \iconv_strpos($haystack, $needle, $offset > 0 ? $offset : 0, $encoding);
9165
            if ($return_tmp !== false) {
9166
                return $return_tmp;
9167
            }
9168
        }
9169
9170
        //
9171
        // fallback for ascii only
9172
        //
9173
9174 4
        if (ASCII::is_ascii($haystack . $needle)) {
9175 2
            return \strpos($haystack, $needle, $offset);
9176
        }
9177
9178
        //
9179
        // fallback via vanilla php
9180
        //
9181
9182 4
        $haystack_tmp = self::substr($haystack, $offset, null, $encoding);
9183 4
        if ($haystack_tmp === false) {
9184
            $haystack_tmp = '';
9185
        }
9186 4
        $haystack = (string) $haystack_tmp;
9187
9188 4
        if ($offset < 0) {
9189
            $offset = 0;
9190
        }
9191
9192 4
        $pos = \strpos($haystack, $needle);
9193 4
        if ($pos === false) {
9194 2
            return false;
9195
        }
9196
9197 4
        if ($pos) {
9198 4
            return $offset + (int) self::strlen(\substr($haystack, 0, $pos), $encoding);
9199
        }
9200
9201 2
        return $offset + 0;
9202
    }
9203
9204
    /**
9205
     * Find the position of the first occurrence of a substring in a string.
9206
     *
9207
     * @param string $haystack <p>
9208
     *                         The string being checked.
9209
     *                         </p>
9210
     * @param string $needle   <p>
9211
     *                         The position counted from the beginning of haystack.
9212
     *                         </p>
9213
     * @param int    $offset   [optional] <p>
9214
     *                         The search offset. If it is not specified, 0 is used.
9215
     *                         </p>
9216
     *
9217
     * @return false|int The numeric position of the first occurrence of needle in the
9218
     *                   haystack string. If needle is not found, it returns false.
9219
     */
9220
    public static function strpos_in_byte(string $haystack, string $needle, int $offset = 0)
9221
    {
9222
        if ($haystack === '' || $needle === '') {
9223
            return false;
9224
        }
9225
9226
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9227
            // "mb_" is available if overload is used, so use it ...
9228
            return \mb_strpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
9229
        }
9230
9231
        return \strpos($haystack, $needle, $offset);
9232
    }
9233
9234
    /**
9235
     * Find the last occurrence of a character in a string within another.
9236
     *
9237
     * @see http://php.net/manual/en/function.mb-strrchr.php
9238
     *
9239
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
9240
     * @param string $needle        <p>The string to find in haystack</p>
9241
     * @param bool   $before_needle [optional] <p>
9242
     *                              Determines which portion of haystack
9243
     *                              this function returns.
9244
     *                              If set to true, it returns all of haystack
9245
     *                              from the beginning to the last occurrence of needle.
9246
     *                              If set to false, it returns all of haystack
9247
     *                              from the last occurrence of needle to the end,
9248
     *                              </p>
9249
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9250
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9251
     *
9252
     * @return false|string the portion of haystack or false if needle is not found
9253
     */
9254
    public static function strrchr(
9255
        string $haystack,
9256
        string $needle,
9257
        bool $before_needle = false,
9258
        string $encoding = 'UTF-8',
9259
        bool $clean_utf8 = false
9260
    ) {
9261 2
        if ($haystack === '' || $needle === '') {
9262 2
            return false;
9263
        }
9264
9265 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9266 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9267
        }
9268
9269 2
        if ($clean_utf8 === true) {
9270
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9271
            // if invalid characters are found in $haystack before $needle
9272 2
            $needle = self::clean($needle);
9273 2
            $haystack = self::clean($haystack);
9274
        }
9275
9276
        //
9277
        // fallback via mbstring
9278
        //
9279
9280 2
        if (self::$SUPPORT['mbstring'] === true) {
9281 2
            if ($encoding === 'UTF-8') {
9282 2
                return \mb_strrchr($haystack, $needle, $before_needle);
9283
            }
9284
9285 2
            return \mb_strrchr($haystack, $needle, $before_needle, $encoding);
9286
        }
9287
9288
        //
9289
        // fallback for binary || ascii only
9290
        //
9291
9292
        if (
9293
            $before_needle === false
9294
            &&
9295
            (
9296
                $encoding === 'CP850'
9297
                ||
9298
                $encoding === 'ASCII'
9299
            )
9300
        ) {
9301
            return \strrchr($haystack, $needle);
9302
        }
9303
9304
        if (
9305
            $encoding !== 'UTF-8'
9306
            &&
9307
            self::$SUPPORT['mbstring'] === false
9308
        ) {
9309
            \trigger_error('UTF8::strrchr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9310
        }
9311
9312
        //
9313
        // fallback via iconv
9314
        //
9315
9316
        if (self::$SUPPORT['iconv'] === true) {
9317
            $needle_tmp = self::substr($needle, 0, 1, $encoding);
9318
            if ($needle_tmp === false) {
9319
                return false;
9320
            }
9321
            $needle = (string) $needle_tmp;
9322
9323
            $pos = \iconv_strrpos($haystack, $needle, $encoding);
9324
            if ($pos === false) {
9325
                return false;
9326
            }
9327
9328
            if ($before_needle) {
9329
                return self::substr($haystack, 0, $pos, $encoding);
9330
            }
9331
9332
            return self::substr($haystack, $pos, null, $encoding);
9333
        }
9334
9335
        //
9336
        // fallback via vanilla php
9337
        //
9338
9339
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
9340
        if ($needle_tmp === false) {
9341
            return false;
9342
        }
9343
        $needle = (string) $needle_tmp;
9344
9345
        $pos = self::strrpos($haystack, $needle, 0, $encoding);
9346
        if ($pos === false) {
9347
            return false;
9348
        }
9349
9350
        if ($before_needle) {
9351
            return self::substr($haystack, 0, $pos, $encoding);
9352
        }
9353
9354
        return self::substr($haystack, $pos, null, $encoding);
9355
    }
9356
9357
    /**
9358
     * Reverses characters order in the string.
9359
     *
9360
     * @param string $str      <p>The input string.</p>
9361
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9362
     *
9363
     * @return string the string with characters in the reverse sequence
9364
     */
9365
    public static function strrev(string $str, string $encoding = 'UTF-8'): string
9366
    {
9367 10
        if ($str === '') {
9368 4
            return '';
9369
        }
9370
9371
        // init
9372 8
        $reversed = '';
9373
9374 8
        $str = self::emoji_encode($str, true);
9375
9376 8
        if ($encoding === 'UTF-8') {
9377 8
            if (self::$SUPPORT['intl'] === true) {
9378
                // try "grapheme" first: https://stackoverflow.com/questions/17496493/strrev-dosent-support-utf-8
9379 8
                $i = (int) \grapheme_strlen($str);
9380 8
                while ($i--) {
9381 8
                    $reversed_tmp = \grapheme_substr($str, $i, 1);
9382 8
                    if ($reversed_tmp !== false) {
9383 8
                        $reversed .= $reversed_tmp;
9384
                    }
9385
                }
9386
            } else {
9387
                $i = (int) \mb_strlen($str);
9388 8
                while ($i--) {
9389
                    $reversed_tmp = \mb_substr($str, $i, 1);
9390
                    if ($reversed_tmp !== false) {
9391
                        $reversed .= $reversed_tmp;
9392
                    }
9393
                }
9394
            }
9395
        } else {
9396
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9397
9398
            $i = (int) self::strlen($str, $encoding);
9399
            while ($i--) {
9400
                $reversed_tmp = self::substr($str, $i, 1, $encoding);
9401
                if ($reversed_tmp !== false) {
9402
                    $reversed .= $reversed_tmp;
9403
                }
9404
            }
9405
        }
9406
9407 8
        return self::emoji_decode($reversed, true);
9408
    }
9409
9410
    /**
9411
     * Find the last occurrence of a character in a string within another, case-insensitive.
9412
     *
9413
     * @see http://php.net/manual/en/function.mb-strrichr.php
9414
     *
9415
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
9416
     * @param string $needle        <p>The string to find in haystack.</p>
9417
     * @param bool   $before_needle [optional] <p>
9418
     *                              Determines which portion of haystack
9419
     *                              this function returns.
9420
     *                              If set to true, it returns all of haystack
9421
     *                              from the beginning to the last occurrence of needle.
9422
     *                              If set to false, it returns all of haystack
9423
     *                              from the last occurrence of needle to the end,
9424
     *                              </p>
9425
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9426
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9427
     *
9428
     * @return false|string the portion of haystack or<br>false if needle is not found
9429
     */
9430
    public static function strrichr(
9431
        string $haystack,
9432
        string $needle,
9433
        bool $before_needle = false,
9434
        string $encoding = 'UTF-8',
9435
        bool $clean_utf8 = false
9436
    ) {
9437 3
        if ($haystack === '' || $needle === '') {
9438 2
            return false;
9439
        }
9440
9441 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9442 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9443
        }
9444
9445 3
        if ($clean_utf8 === true) {
9446
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9447
            // if invalid characters are found in $haystack before $needle
9448 2
            $needle = self::clean($needle);
9449 2
            $haystack = self::clean($haystack);
9450
        }
9451
9452
        //
9453
        // fallback via mbstring
9454
        //
9455
9456 3
        if (self::$SUPPORT['mbstring'] === true) {
9457 3
            if ($encoding === 'UTF-8') {
9458 3
                return \mb_strrichr($haystack, $needle, $before_needle);
9459
            }
9460
9461 2
            return \mb_strrichr($haystack, $needle, $before_needle, $encoding);
9462
        }
9463
9464
        //
9465
        // fallback via vanilla php
9466
        //
9467
9468
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
9469
        if ($needle_tmp === false) {
9470
            return false;
9471
        }
9472
        $needle = (string) $needle_tmp;
9473
9474
        $pos = self::strripos($haystack, $needle, 0, $encoding);
9475
        if ($pos === false) {
9476
            return false;
9477
        }
9478
9479
        if ($before_needle) {
9480
            return self::substr($haystack, 0, $pos, $encoding);
9481
        }
9482
9483
        return self::substr($haystack, $pos, null, $encoding);
9484
    }
9485
9486
    /**
9487
     * Find the position of the last occurrence of a substring in a string, case-insensitive.
9488
     *
9489
     * @param string     $haystack   <p>The string to look in.</p>
9490
     * @param int|string $needle     <p>The string to look for.</p>
9491
     * @param int        $offset     [optional] <p>Number of characters to ignore in the beginning or end.</p>
9492
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9493
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9494
     *
9495
     * @return false|int
9496
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
9497
     *                   string.<br>If needle is not found, it returns false.</p>
9498
     */
9499
    public static function strripos(
9500
        string $haystack,
9501
        $needle,
9502
        int $offset = 0,
9503
        string $encoding = 'UTF-8',
9504
        bool $clean_utf8 = false
9505
    ) {
9506 3
        if ($haystack === '') {
9507
            return false;
9508
        }
9509
9510
        // iconv and mbstring do not support integer $needle
9511 3
        if ((int) $needle === $needle && $needle >= 0) {
9512
            $needle = (string) self::chr($needle);
9513
        }
9514 3
        $needle = (string) $needle;
9515
9516 3
        if ($needle === '') {
9517
            return false;
9518
        }
9519
9520 3
        if ($clean_utf8 === true) {
9521
            // mb_strripos() && iconv_strripos() is not tolerant to invalid characters
9522 2
            $needle = self::clean($needle);
9523 2
            $haystack = self::clean($haystack);
9524
        }
9525
9526 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9527 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9528
        }
9529
9530
        //
9531
        // fallback via mbstrig
9532
        //
9533
9534 3
        if (self::$SUPPORT['mbstring'] === true) {
9535 3
            if ($encoding === 'UTF-8') {
9536 3
                return \mb_strripos($haystack, $needle, $offset);
9537
            }
9538
9539
            return \mb_strripos($haystack, $needle, $offset, $encoding);
9540
        }
9541
9542
        //
9543
        // fallback for binary || ascii only
9544
        //
9545
9546
        if (
9547
            $encoding === 'CP850'
9548
            ||
9549
            $encoding === 'ASCII'
9550
        ) {
9551
            return \strripos($haystack, $needle, $offset);
9552
        }
9553
9554
        if (
9555
            $encoding !== 'UTF-8'
9556
            &&
9557
            self::$SUPPORT['mbstring'] === false
9558
        ) {
9559
            \trigger_error('UTF8::strripos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9560
        }
9561
9562
        //
9563
        // fallback via intl
9564
        //
9565
9566
        if (
9567
            $encoding === 'UTF-8' // INFO: "grapheme_strripos()" can't handle other encodings
9568
            &&
9569
            $offset >= 0 // grapheme_strripos() can't handle negative offset
9570
            &&
9571
            self::$SUPPORT['intl'] === true
9572
        ) {
9573
            $return_tmp = \grapheme_strripos($haystack, $needle, $offset);
9574
            if ($return_tmp !== false) {
9575
                return $return_tmp;
9576
            }
9577
        }
9578
9579
        //
9580
        // fallback for ascii only
9581
        //
9582
9583
        if (ASCII::is_ascii($haystack . $needle)) {
9584
            return \strripos($haystack, $needle, $offset);
9585
        }
9586
9587
        //
9588
        // fallback via vanilla php
9589
        //
9590
9591
        $haystack = self::strtocasefold($haystack, true, false, $encoding);
9592
        $needle = self::strtocasefold($needle, true, false, $encoding);
9593
9594
        return self::strrpos($haystack, $needle, $offset, $encoding, $clean_utf8);
9595
    }
9596
9597
    /**
9598
     * Finds position of last occurrence of a string within another, case-insensitive.
9599
     *
9600
     * @param string $haystack <p>
9601
     *                         The string from which to get the position of the last occurrence
9602
     *                         of needle.
9603
     *                         </p>
9604
     * @param string $needle   <p>
9605
     *                         The string to find in haystack.
9606
     *                         </p>
9607
     * @param int    $offset   [optional] <p>
9608
     *                         The position in haystack
9609
     *                         to start searching.
9610
     *                         </p>
9611
     *
9612
     * @return false|int
9613
     *                   <p>eturn the numeric position of the last occurrence of needle in the
9614
     *                   haystack string, or false if needle is not found.</p>
9615
     */
9616
    public static function strripos_in_byte(string $haystack, string $needle, int $offset = 0)
9617
    {
9618
        if ($haystack === '' || $needle === '') {
9619
            return false;
9620
        }
9621
9622
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9623
            // "mb_" is available if overload is used, so use it ...
9624
            return \mb_strripos($haystack, $needle, $offset, 'CP850'); // 8-BIT
9625
        }
9626
9627
        return \strripos($haystack, $needle, $offset);
9628
    }
9629
9630
    /**
9631
     * Find the position of the last occurrence of a substring in a string.
9632
     *
9633
     * @see http://php.net/manual/en/function.mb-strrpos.php
9634
     *
9635
     * @param string     $haystack   <p>The string being checked, for the last occurrence of needle</p>
9636
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
9637
     * @param int        $offset     [optional] <p>May be specified to begin searching an arbitrary number of characters
9638
     *                               into the string. Negative values will stop searching at an arbitrary point prior to
9639
     *                               the end of the string.
9640
     *                               </p>
9641
     * @param string     $encoding   [optional] <p>Set the charset.</p>
9642
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9643
     *
9644
     * @return false|int
9645
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
9646
     *                   string.<br>If needle is not found, it returns false.</p>
9647
     */
9648
    public static function strrpos(
9649
        string $haystack,
9650
        $needle,
9651
        int $offset = 0,
9652
        string $encoding = 'UTF-8',
9653
        bool $clean_utf8 = false
9654
    ) {
9655 35
        if ($haystack === '') {
9656 3
            return false;
9657
        }
9658
9659
        // iconv and mbstring do not support integer $needle
9660 34
        if ((int) $needle === $needle && $needle >= 0) {
9661 2
            $needle = (string) self::chr($needle);
9662
        }
9663 34
        $needle = (string) $needle;
9664
9665 34
        if ($needle === '') {
9666 2
            return false;
9667
        }
9668
9669 34
        if ($clean_utf8 === true) {
9670
            // mb_strrpos && iconv_strrpos is not tolerant to invalid characters
9671 4
            $needle = self::clean($needle);
9672 4
            $haystack = self::clean($haystack);
9673
        }
9674
9675 34
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9676 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9677
        }
9678
9679
        //
9680
        // fallback via mbstring
9681
        //
9682
9683 34
        if (self::$SUPPORT['mbstring'] === true) {
9684 34
            if ($encoding === 'UTF-8') {
9685 34
                return \mb_strrpos($haystack, $needle, $offset);
9686
            }
9687
9688 2
            return \mb_strrpos($haystack, $needle, $offset, $encoding);
9689
        }
9690
9691
        //
9692
        // fallback for binary || ascii only
9693
        //
9694
9695
        if (
9696
            $encoding === 'CP850'
9697
            ||
9698
            $encoding === 'ASCII'
9699
        ) {
9700
            return \strrpos($haystack, $needle, $offset);
9701
        }
9702
9703
        if (
9704
            $encoding !== 'UTF-8'
9705
            &&
9706
            self::$SUPPORT['mbstring'] === false
9707
        ) {
9708
            \trigger_error('UTF8::strrpos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9709
        }
9710
9711
        //
9712
        // fallback via intl
9713
        //
9714
9715
        if (
9716
            $offset >= 0 // grapheme_strrpos() can't handle negative offset
9717
            &&
9718
            $encoding === 'UTF-8' // INFO: "grapheme_strrpos()" can't handle other encodings
9719
            &&
9720
            self::$SUPPORT['intl'] === true
9721
        ) {
9722
            $return_tmp = \grapheme_strrpos($haystack, $needle, $offset);
9723
            if ($return_tmp !== false) {
9724
                return $return_tmp;
9725
            }
9726
        }
9727
9728
        //
9729
        // fallback for ascii only
9730
        //
9731
9732
        if (ASCII::is_ascii($haystack . $needle)) {
9733
            return \strrpos($haystack, $needle, $offset);
9734
        }
9735
9736
        //
9737
        // fallback via vanilla php
9738
        //
9739
9740
        $haystack_tmp = null;
9741
        if ($offset > 0) {
9742
            $haystack_tmp = self::substr($haystack, $offset);
9743
        } elseif ($offset < 0) {
9744
            $haystack_tmp = self::substr($haystack, 0, $offset);
9745
            $offset = 0;
9746
        }
9747
9748
        if ($haystack_tmp !== null) {
9749
            if ($haystack_tmp === false) {
9750
                $haystack_tmp = '';
9751
            }
9752
            $haystack = (string) $haystack_tmp;
9753
        }
9754
9755
        $pos = \strrpos($haystack, $needle);
9756
        if ($pos === false) {
9757
            return false;
9758
        }
9759
9760
        /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
9761
        $str_tmp = \substr($haystack, 0, $pos);
9762
        if ($str_tmp === false) {
9763
            return false;
9764
        }
9765
9766
        return $offset + (int) self::strlen($str_tmp);
9767
    }
9768
9769
    /**
9770
     * Find the position of the last occurrence of a substring in a string.
9771
     *
9772
     * @param string $haystack <p>
9773
     *                         The string being checked, for the last occurrence
9774
     *                         of needle.
9775
     *                         </p>
9776
     * @param string $needle   <p>
9777
     *                         The string to find in haystack.
9778
     *                         </p>
9779
     * @param int    $offset   [optional] <p>May be specified to begin searching an arbitrary number of characters into
9780
     *                         the string. Negative values will stop searching at an arbitrary point
9781
     *                         prior to the end of the string.
9782
     *                         </p>
9783
     *
9784
     * @return false|int
9785
     *                   <p>The numeric position of the last occurrence of needle in the
9786
     *                   haystack string. If needle is not found, it returns false.</p>
9787
     */
9788
    public static function strrpos_in_byte(string $haystack, string $needle, int $offset = 0)
9789
    {
9790
        if ($haystack === '' || $needle === '') {
9791
            return false;
9792
        }
9793
9794
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9795
            // "mb_" is available if overload is used, so use it ...
9796
            return \mb_strrpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
9797
        }
9798
9799
        return \strrpos($haystack, $needle, $offset);
9800
    }
9801
9802
    /**
9803
     * Finds the length of the initial segment of a string consisting entirely of characters contained within a given
9804
     * mask.
9805
     *
9806
     * @param string $str      <p>The input string.</p>
9807
     * @param string $mask     <p>The mask of chars</p>
9808
     * @param int    $offset   [optional]
9809
     * @param int    $length   [optional]
9810
     * @param string $encoding [optional] <p>Set the charset.</p>
9811
     *
9812
     * @return false|int
9813
     */
9814
    public static function strspn(
9815
        string $str,
9816
        string $mask,
9817
        int $offset = 0,
9818
        int $length = null,
9819
        string $encoding = 'UTF-8'
9820
    ) {
9821 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9822
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9823
        }
9824
9825 10
        if ($offset || $length !== null) {
9826 2
            if ($encoding === 'UTF-8') {
9827 2
                if ($length === null) {
9828
                    $str = (string) \mb_substr($str, $offset);
9829
                } else {
9830 2
                    $str = (string) \mb_substr($str, $offset, $length);
9831
                }
9832
            } else {
9833
                $str = (string) self::substr($str, $offset, $length, $encoding);
9834
            }
9835
        }
9836
9837 10
        if ($str === '' || $mask === '') {
9838 2
            return 0;
9839
        }
9840
9841 8
        $matches = [];
9842
9843 8
        return \preg_match('/^' . self::rxClass($mask) . '+/u', $str, $matches) ? (int) self::strlen($matches[0], $encoding) : 0;
9844
    }
9845
9846
    /**
9847
     * Returns part of haystack string from the first occurrence of needle to the end of haystack.
9848
     *
9849
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
9850
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
9851
     * @param bool   $before_needle [optional] <p>
9852
     *                              If <b>TRUE</b>, strstr() returns the part of the
9853
     *                              haystack before the first occurrence of the needle (excluding the needle).
9854
     *                              </p>
9855
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9856
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9857
     *
9858
     * @return false|string
9859
     *                      A sub-string,<br>or <strong>false</strong> if needle is not found
9860
     */
9861
    public static function strstr(
9862
        string $haystack,
9863
        string $needle,
9864
        bool $before_needle = false,
9865
        string $encoding = 'UTF-8',
9866
        $clean_utf8 = false
9867
    ) {
9868 3
        if ($haystack === '' || $needle === '') {
9869 2
            return false;
9870
        }
9871
9872 3
        if ($clean_utf8 === true) {
9873
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9874
            // if invalid characters are found in $haystack before $needle
9875
            $needle = self::clean($needle);
9876
            $haystack = self::clean($haystack);
9877
        }
9878
9879 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9880 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9881
        }
9882
9883
        //
9884
        // fallback via mbstring
9885
        //
9886
9887 3
        if (self::$SUPPORT['mbstring'] === true) {
9888 3
            if ($encoding === 'UTF-8') {
9889 3
                return \mb_strstr($haystack, $needle, $before_needle);
9890
            }
9891
9892 2
            return \mb_strstr($haystack, $needle, $before_needle, $encoding);
9893
        }
9894
9895
        //
9896
        // fallback for binary || ascii only
9897
        //
9898
9899
        if (
9900
            $encoding === 'CP850'
9901
            ||
9902
            $encoding === 'ASCII'
9903
        ) {
9904
            return \strstr($haystack, $needle, $before_needle);
9905
        }
9906
9907
        if (
9908
            $encoding !== 'UTF-8'
9909
            &&
9910
            self::$SUPPORT['mbstring'] === false
9911
        ) {
9912
            \trigger_error('UTF8::strstr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9913
        }
9914
9915
        //
9916
        // fallback via intl
9917
        //
9918
9919
        if (
9920
            $encoding === 'UTF-8' // INFO: "grapheme_strstr()" can't handle other encodings
9921
            &&
9922
            self::$SUPPORT['intl'] === true
9923
        ) {
9924
            $return_tmp = \grapheme_strstr($haystack, $needle, $before_needle);
9925
            if ($return_tmp !== false) {
9926
                return $return_tmp;
9927
            }
9928
        }
9929
9930
        //
9931
        // fallback for ascii only
9932
        //
9933
9934
        if (ASCII::is_ascii($haystack . $needle)) {
9935
            return \strstr($haystack, $needle, $before_needle);
9936
        }
9937
9938
        //
9939
        // fallback via vanilla php
9940
        //
9941
9942
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/us', $haystack, $match);
9943
9944
        if (!isset($match[1])) {
9945
            return false;
9946
        }
9947
9948
        if ($before_needle) {
9949
            return $match[1];
9950
        }
9951
9952
        return self::substr($haystack, (int) self::strlen($match[1]));
9953
    }
9954
9955
    /**
9956
     *  * Finds first occurrence of a string within another.
9957
     *
9958
     * @param string $haystack      <p>
9959
     *                              The string from which to get the first occurrence
9960
     *                              of needle.
9961
     *                              </p>
9962
     * @param string $needle        <p>
9963
     *                              The string to find in haystack.
9964
     *                              </p>
9965
     * @param bool   $before_needle [optional] <p>
9966
     *                              Determines which portion of haystack
9967
     *                              this function returns.
9968
     *                              If set to true, it returns all of haystack
9969
     *                              from the beginning to the first occurrence of needle.
9970
     *                              If set to false, it returns all of haystack
9971
     *                              from the first occurrence of needle to the end,
9972
     *                              </p>
9973
     *
9974
     * @return false|string
9975
     *                      <p>The portion of haystack,
9976
     *                      or false if needle is not found.</p>
9977
     */
9978
    public static function strstr_in_byte(
9979
        string $haystack,
9980
        string $needle,
9981
        bool $before_needle = false
9982
    ) {
9983
        if ($haystack === '' || $needle === '') {
9984
            return false;
9985
        }
9986
9987
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9988
            // "mb_" is available if overload is used, so use it ...
9989
            return \mb_strstr($haystack, $needle, $before_needle, 'CP850'); // 8-BIT
9990
        }
9991
9992
        return \strstr($haystack, $needle, $before_needle);
9993
    }
9994
9995
    /**
9996
     * Unicode transformation for case-less matching.
9997
     *
9998
     * @see http://unicode.org/reports/tr21/tr21-5.html
9999
     *
10000
     * @param string      $str        <p>The input string.</p>
10001
     * @param bool        $full       [optional] <p>
10002
     *                                <b>true</b>, replace full case folding chars (default)<br>
10003
     *                                <b>false</b>, use only limited static array [UTF8::$COMMON_CASE_FOLD]
10004
     *                                </p>
10005
     * @param bool        $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10006
     * @param string      $encoding   [optional] <p>Set the charset.</p>
10007
     * @param string|null $lang       [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10008
     * @param bool        $lower      [optional] <p>Use lowercase string, otherwise use uppercase string. PS: uppercase
10009
     *                                is for some languages better ...</p>
10010
     *
10011
     * @return string
10012
     */
10013
    public static function strtocasefold(
10014
        string $str,
10015
        bool $full = true,
10016
        bool $clean_utf8 = false,
10017
        string $encoding = 'UTF-8',
10018
        string $lang = null,
10019
        $lower = true
10020
    ): string {
10021 32
        if ($str === '') {
10022 5
            return '';
10023
        }
10024
10025 31
        if ($clean_utf8 === true) {
10026
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10027
            // if invalid characters are found in $haystack before $needle
10028 2
            $str = self::clean($str);
10029
        }
10030
10031 31
        $str = self::fixStrCaseHelper($str, $lower, $full);
10032
10033 31
        if ($lang === null && $encoding === 'UTF-8') {
10034 31
            if ($lower === true) {
10035 2
                return \mb_strtolower($str);
10036
            }
10037
10038 29
            return \mb_strtoupper($str);
10039
        }
10040
10041 2
        if ($lower === true) {
10042
            return self::strtolower($str, $encoding, false, $lang);
10043
        }
10044
10045 2
        return self::strtoupper($str, $encoding, false, $lang);
10046
    }
10047
10048
    /**
10049
     * Make a string lowercase.
10050
     *
10051
     * @see http://php.net/manual/en/function.mb-strtolower.php
10052
     *
10053
     * @param string      $str                           <p>The string being lowercased.</p>
10054
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
10055
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
10056
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10057
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
10058
     *
10059
     * @return string
10060
     *                <p>String with all alphabetic characters converted to lowercase.</p>
10061
     */
10062
    public static function strtolower(
10063
        $str,
10064
        string $encoding = 'UTF-8',
10065
        bool $clean_utf8 = false,
10066
        string $lang = null,
10067
        bool $try_to_keep_the_string_length = false
10068
    ): string {
10069
        // init
10070 73
        $str = (string) $str;
10071
10072 73
        if ($str === '') {
10073 1
            return '';
10074
        }
10075
10076 72
        if ($clean_utf8 === true) {
10077
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10078
            // if invalid characters are found in $haystack before $needle
10079 2
            $str = self::clean($str);
10080
        }
10081
10082
        // hack for old php version or for the polyfill ...
10083 72
        if ($try_to_keep_the_string_length === true) {
10084
            $str = self::fixStrCaseHelper($str, true);
10085
        }
10086
10087 72
        if ($lang === null && $encoding === 'UTF-8') {
10088 13
            return \mb_strtolower($str);
10089
        }
10090
10091 61
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10092
10093 61
        if ($lang !== null) {
10094 2
            if (self::$SUPPORT['intl'] === true) {
10095 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
10096
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
10097
                }
10098
10099 2
                $language_code = $lang . '-Lower';
10100 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
10101
                    \trigger_error('UTF8::strtolower() cannot handle special language: ' . $lang . ' | supported: ' . \print_r(self::$INTL_TRANSLITERATOR_LIST, true), \E_USER_WARNING);
10102
10103
                    $language_code = 'Any-Lower';
10104
                }
10105
10106
                /** @noinspection PhpComposerExtensionStubsInspection */
10107
                /** @noinspection UnnecessaryCastingInspection */
10108 2
                return (string) \transliterator_transliterate($language_code, $str);
10109
            }
10110
10111
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang" parameter: ' . $lang, \E_USER_WARNING);
10112
        }
10113
10114
        // always fallback via symfony polyfill
10115 61
        return \mb_strtolower($str, $encoding);
10116
    }
10117
10118
    /**
10119
     * Make a string uppercase.
10120
     *
10121
     * @see http://php.net/manual/en/function.mb-strtoupper.php
10122
     *
10123
     * @param string      $str                           <p>The string being uppercased.</p>
10124
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
10125
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
10126
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10127
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
10128
     *
10129
     * @return string
10130
     *                <p>String with all alphabetic characters converted to uppercase.</p>
10131
     */
10132
    public static function strtoupper(
10133
        $str,
10134
        string $encoding = 'UTF-8',
10135
        bool $clean_utf8 = false,
10136
        string $lang = null,
10137
        bool $try_to_keep_the_string_length = false
10138
    ): string {
10139
        // init
10140 17
        $str = (string) $str;
10141
10142 17
        if ($str === '') {
10143 1
            return '';
10144
        }
10145
10146 16
        if ($clean_utf8 === true) {
10147
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10148
            // if invalid characters are found in $haystack before $needle
10149 2
            $str = self::clean($str);
10150
        }
10151
10152
        // hack for old php version or for the polyfill ...
10153 16
        if ($try_to_keep_the_string_length === true) {
10154 2
            $str = self::fixStrCaseHelper($str, false);
10155
        }
10156
10157 16
        if ($lang === null && $encoding === 'UTF-8') {
10158 8
            return \mb_strtoupper($str);
10159
        }
10160
10161 10
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10162
10163 10
        if ($lang !== null) {
10164 2
            if (self::$SUPPORT['intl'] === true) {
10165 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
10166
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
10167
                }
10168
10169 2
                $language_code = $lang . '-Upper';
10170 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
10171
                    \trigger_error('UTF8::strtoupper() without intl for special language: ' . $lang, \E_USER_WARNING);
10172
10173
                    $language_code = 'Any-Upper';
10174
                }
10175
10176
                /** @noinspection PhpComposerExtensionStubsInspection */
10177
                /** @noinspection UnnecessaryCastingInspection */
10178 2
                return (string) \transliterator_transliterate($language_code, $str);
10179
            }
10180
10181
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang"-parameter: ' . $lang, \E_USER_WARNING);
10182
        }
10183
10184
        // always fallback via symfony polyfill
10185 10
        return \mb_strtoupper($str, $encoding);
10186
    }
10187
10188
    /**
10189
     * Translate characters or replace sub-strings.
10190
     *
10191
     * @see http://php.net/manual/en/function.strtr.php
10192
     *
10193
     * @param string          $str  <p>The string being translated.</p>
10194
     * @param string|string[] $from <p>The string replacing from.</p>
10195
     * @param string|string[] $to   [optional] <p>The string being translated to to.</p>
10196
     *
10197
     * @return string
10198
     *                <p>This function returns a copy of str, translating all occurrences of each character in "from" to the
10199
     *                corresponding character in "to".</p>
10200
     */
10201
    public static function strtr(string $str, $from, $to = ''): string
10202
    {
10203 2
        if ($str === '') {
10204
            return '';
10205
        }
10206
10207 2
        if ($from === $to) {
10208
            return $str;
10209
        }
10210
10211 2
        if ($to !== '') {
10212 2
            $from = self::str_split($from);
10213 2
            $to = self::str_split($to);
10214 2
            $count_from = \count($from);
10215 2
            $count_to = \count($to);
10216
10217 2
            if ($count_from > $count_to) {
10218 2
                $from = \array_slice($from, 0, $count_to);
10219 2
            } elseif ($count_from < $count_to) {
10220 2
                $to = \array_slice($to, 0, $count_from);
10221
            }
10222
10223 2
            $from = \array_combine($from, $to);
10224
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
10225 2
            if ($from === false) {
10226
                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) . ')');
10227
            }
10228
        }
10229
10230 2
        if (\is_string($from)) {
10231 2
            return \str_replace($from, '', $str);
10232
        }
10233
10234 2
        return \strtr($str, $from);
10235
    }
10236
10237
    /**
10238
     * Return the width of a string.
10239
     *
10240
     * @param string $str        <p>The input string.</p>
10241
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10242
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10243
     *
10244
     * @return int
10245
     */
10246
    public static function strwidth(
10247
        string $str,
10248
        string $encoding = 'UTF-8',
10249
        bool $clean_utf8 = false
10250
    ): int {
10251 2
        if ($str === '') {
10252 2
            return 0;
10253
        }
10254
10255 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10256 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10257
        }
10258
10259 2
        if ($clean_utf8 === true) {
10260
            // iconv and mbstring are not tolerant to invalid encoding
10261
            // further, their behaviour is inconsistent with that of PHP's substr
10262 2
            $str = self::clean($str);
10263
        }
10264
10265
        //
10266
        // fallback via mbstring
10267
        //
10268
10269 2
        if (self::$SUPPORT['mbstring'] === true) {
10270 2
            if ($encoding === 'UTF-8') {
10271 2
                return \mb_strwidth($str);
10272
            }
10273
10274
            return \mb_strwidth($str, $encoding);
10275
        }
10276
10277
        //
10278
        // fallback via vanilla php
10279
        //
10280
10281
        if ($encoding !== 'UTF-8') {
10282
            $str = self::encode('UTF-8', $str, false, $encoding);
10283
        }
10284
10285
        $wide = 0;
10286
        $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);
10287
10288
        return ($wide << 1) + (int) self::strlen($str, 'UTF-8');
10289
    }
10290
10291
    /**
10292
     * Get part of a string.
10293
     *
10294
     * @see http://php.net/manual/en/function.mb-substr.php
10295
     *
10296
     * @param string $str        <p>The string being checked.</p>
10297
     * @param int    $offset     <p>The first position used in str.</p>
10298
     * @param int    $length     [optional] <p>The maximum length of the returned string.</p>
10299
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10300
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10301
     *
10302
     * @return false|string
10303
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
10304
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
10305
     *                      characters long, <b>FALSE</b> will be returned.
10306
     */
10307
    public static function substr(
10308
        string $str,
10309
        int $offset = 0,
10310
        int $length = null,
10311
        string $encoding = 'UTF-8',
10312
        bool $clean_utf8 = false
10313
    ) {
10314
        // empty string
10315 172
        if ($str === '' || $length === 0) {
10316 8
            return '';
10317
        }
10318
10319 168
        if ($clean_utf8 === true) {
10320
            // iconv and mbstring are not tolerant to invalid encoding
10321
            // further, their behaviour is inconsistent with that of PHP's substr
10322 2
            $str = self::clean($str);
10323
        }
10324
10325
        // whole string
10326 168
        if (!$offset && $length === null) {
10327 7
            return $str;
10328
        }
10329
10330 163
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10331 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10332
        }
10333
10334
        //
10335
        // fallback via mbstring
10336
        //
10337
10338 163
        if (self::$SUPPORT['mbstring'] === true) {
10339 161
            if ($encoding === 'UTF-8') {
10340 161
                if ($length === null) {
10341 64
                    return \mb_substr($str, $offset);
10342
                }
10343
10344 102
                return \mb_substr($str, $offset, $length);
10345
            }
10346
10347
            return self::substr($str, $offset, $length, $encoding);
10348
        }
10349
10350
        //
10351
        // fallback for binary || ascii only
10352
        //
10353
10354
        if (
10355 4
            $encoding === 'CP850'
10356
            ||
10357 4
            $encoding === 'ASCII'
10358
        ) {
10359
            if ($length === null) {
10360
                return \substr($str, $offset);
10361
            }
10362
10363
            return \substr($str, $offset, $length);
10364
        }
10365
10366
        // otherwise we need the string-length
10367 4
        $str_length = 0;
10368 4
        if ($offset || $length === null) {
10369 4
            $str_length = self::strlen($str, $encoding);
10370
        }
10371
10372
        // e.g.: invalid chars + mbstring not installed
10373 4
        if ($str_length === false) {
10374
            return false;
10375
        }
10376
10377
        // empty string
10378 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...
10379
            return '';
10380
        }
10381
10382
        // impossible
10383 4
        if ($offset && $offset > $str_length) {
10384
            return '';
10385
        }
10386
10387 4
        if ($length === null) {
10388 4
            $length = (int) $str_length;
10389
        } else {
10390 2
            $length = (int) $length;
10391
        }
10392
10393
        if (
10394 4
            $encoding !== 'UTF-8'
10395
            &&
10396 4
            self::$SUPPORT['mbstring'] === false
10397
        ) {
10398 2
            \trigger_error('UTF8::substr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10399
        }
10400
10401
        //
10402
        // fallback via intl
10403
        //
10404
10405
        if (
10406 4
            $encoding === 'UTF-8' // INFO: "grapheme_substr()" can't handle other encodings
10407
            &&
10408 4
            $offset >= 0 // grapheme_substr() can't handle negative offset
10409
            &&
10410 4
            self::$SUPPORT['intl'] === true
10411
        ) {
10412
            $return_tmp = \grapheme_substr($str, $offset, $length);
10413
            if ($return_tmp !== false) {
10414
                return $return_tmp;
10415
            }
10416
        }
10417
10418
        //
10419
        // fallback via iconv
10420
        //
10421
10422
        if (
10423 4
            $length >= 0 // "iconv_substr()" can't handle negative length
10424
            &&
10425 4
            self::$SUPPORT['iconv'] === true
10426
        ) {
10427
            $return_tmp = \iconv_substr($str, $offset, $length);
10428
            if ($return_tmp !== false) {
10429
                return $return_tmp;
10430
            }
10431
        }
10432
10433
        //
10434
        // fallback for ascii only
10435
        //
10436
10437 4
        if (ASCII::is_ascii($str)) {
10438
            return \substr($str, $offset, $length);
10439
        }
10440
10441
        //
10442
        // fallback via vanilla php
10443
        //
10444
10445
        // split to array, and remove invalid characters
10446 4
        $array = self::str_split($str);
10447
10448
        // extract relevant part, and join to make sting again
10449 4
        return \implode('', \array_slice($array, $offset, $length));
10450
    }
10451
10452
    /**
10453
     * Binary-safe comparison of two strings from an offset, up to a length of characters.
10454
     *
10455
     * @param string   $str1               <p>The main string being compared.</p>
10456
     * @param string   $str2               <p>The secondary string being compared.</p>
10457
     * @param int      $offset             [optional] <p>The start position for the comparison. If negative, it starts
10458
     *                                     counting from the end of the string.</p>
10459
     * @param int|null $length             [optional] <p>The length of the comparison. The default value is the largest
10460
     *                                     of the length of the str compared to the length of main_str less the
10461
     *                                     offset.</p>
10462
     * @param bool     $case_insensitivity [optional] <p>If case_insensitivity is TRUE, comparison is case
10463
     *                                     insensitive.</p>
10464
     * @param string   $encoding           [optional] <p>Set the charset for e.g. "mb_" function</p>
10465
     *
10466
     * @return int
10467
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
10468
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
10469
     *             <strong>0</strong> if they are equal
10470
     */
10471
    public static function substr_compare(
10472
        string $str1,
10473
        string $str2,
10474
        int $offset = 0,
10475
        int $length = null,
10476
        bool $case_insensitivity = false,
10477
        string $encoding = 'UTF-8'
10478
    ): int {
10479
        if (
10480 2
            $offset !== 0
10481
            ||
10482 2
            $length !== null
10483
        ) {
10484 2
            if ($encoding === 'UTF-8') {
10485 2
                if ($length === null) {
10486 2
                    $str1 = (string) \mb_substr($str1, $offset);
10487
                } else {
10488 2
                    $str1 = (string) \mb_substr($str1, $offset, $length);
10489
                }
10490 2
                $str2 = (string) \mb_substr($str2, 0, (int) self::strlen($str1));
10491
            } else {
10492
                $encoding = self::normalize_encoding($encoding, 'UTF-8');
10493
10494
                $str1 = (string) self::substr($str1, $offset, $length, $encoding);
10495
                $str2 = (string) self::substr($str2, 0, (int) self::strlen($str1), $encoding);
10496
            }
10497
        }
10498
10499 2
        if ($case_insensitivity === true) {
10500 2
            return self::strcasecmp($str1, $str2, $encoding);
10501
        }
10502
10503 2
        return self::strcmp($str1, $str2);
10504
    }
10505
10506
    /**
10507
     * Count the number of substring occurrences.
10508
     *
10509
     * @see http://php.net/manual/en/function.substr-count.php
10510
     *
10511
     * @param string $haystack   <p>The string to search in.</p>
10512
     * @param string $needle     <p>The substring to search for.</p>
10513
     * @param int    $offset     [optional] <p>The offset where to start counting.</p>
10514
     * @param int    $length     [optional] <p>
10515
     *                           The maximum length after the specified offset to search for the
10516
     *                           substring. It outputs a warning if the offset plus the length is
10517
     *                           greater than the haystack length.
10518
     *                           </p>
10519
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10520
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10521
     *
10522
     * @return false|int this functions returns an integer or false if there isn't a string
10523
     */
10524
    public static function substr_count(
10525
        string $haystack,
10526
        string $needle,
10527
        int $offset = 0,
10528
        int $length = null,
10529
        string $encoding = 'UTF-8',
10530
        bool $clean_utf8 = false
10531
    ) {
10532 5
        if ($haystack === '' || $needle === '') {
10533 2
            return false;
10534
        }
10535
10536 5
        if ($length === 0) {
10537 2
            return 0;
10538
        }
10539
10540 5
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10541 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10542
        }
10543
10544 5
        if ($clean_utf8 === true) {
10545
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10546
            // if invalid characters are found in $haystack before $needle
10547
            $needle = self::clean($needle);
10548
            $haystack = self::clean($haystack);
10549
        }
10550
10551 5
        if ($offset || $length > 0) {
10552 2
            if ($length === null) {
10553 2
                $length_tmp = self::strlen($haystack, $encoding);
10554 2
                if ($length_tmp === false) {
10555
                    return false;
10556
                }
10557 2
                $length = (int) $length_tmp;
10558
            }
10559
10560 2
            if ($encoding === 'UTF-8') {
10561 2
                $haystack = (string) \mb_substr($haystack, $offset, $length);
10562
            } else {
10563 2
                $haystack = (string) \mb_substr($haystack, $offset, $length, $encoding);
10564
            }
10565
        }
10566
10567
        if (
10568 5
            $encoding !== 'UTF-8'
10569
            &&
10570 5
            self::$SUPPORT['mbstring'] === false
10571
        ) {
10572
            \trigger_error('UTF8::substr_count() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10573
        }
10574
10575 5
        if (self::$SUPPORT['mbstring'] === true) {
10576 5
            if ($encoding === 'UTF-8') {
10577 5
                return \mb_substr_count($haystack, $needle);
10578
            }
10579
10580 2
            return \mb_substr_count($haystack, $needle, $encoding);
10581
        }
10582
10583
        \preg_match_all('/' . \preg_quote($needle, '/') . '/us', $haystack, $matches, \PREG_SET_ORDER);
10584
10585
        return \count($matches);
10586
    }
10587
10588
    /**
10589
     * Count the number of substring occurrences.
10590
     *
10591
     * @param string $haystack <p>
10592
     *                         The string being checked.
10593
     *                         </p>
10594
     * @param string $needle   <p>
10595
     *                         The string being found.
10596
     *                         </p>
10597
     * @param int    $offset   [optional] <p>
10598
     *                         The offset where to start counting
10599
     *                         </p>
10600
     * @param int    $length   [optional] <p>
10601
     *                         The maximum length after the specified offset to search for the
10602
     *                         substring. It outputs a warning if the offset plus the length is
10603
     *                         greater than the haystack length.
10604
     *                         </p>
10605
     *
10606
     * @return false|int the number of times the
10607
     *                   needle substring occurs in the
10608
     *                   haystack string
10609
     */
10610
    public static function substr_count_in_byte(
10611
        string $haystack,
10612
        string $needle,
10613
        int $offset = 0,
10614
        int $length = null
10615
    ) {
10616
        if ($haystack === '' || $needle === '') {
10617
            return 0;
10618
        }
10619
10620
        if (
10621
            ($offset || $length !== null)
10622
            &&
10623
            self::$SUPPORT['mbstring_func_overload'] === true
10624
        ) {
10625
            if ($length === null) {
10626
                $length_tmp = self::strlen($haystack);
10627
                if ($length_tmp === false) {
10628
                    return false;
10629
                }
10630
                $length = (int) $length_tmp;
10631
            }
10632
10633
            if (
10634
                (
10635
                    $length !== 0
10636
                    &&
10637
                    $offset !== 0
10638
                )
10639
                &&
10640
                ($length + $offset) <= 0
10641
                &&
10642
                Bootup::is_php('7.1') === false // output from "substr_count()" have changed in PHP 7.1
10643
            ) {
10644
                return false;
10645
            }
10646
10647
            /** @var false|string $haystack_tmp - needed for PhpStan (stubs error) */
10648
            $haystack_tmp = \substr($haystack, $offset, $length);
10649
            if ($haystack_tmp === false) {
10650
                $haystack_tmp = '';
10651
            }
10652
            $haystack = (string) $haystack_tmp;
10653
        }
10654
10655
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10656
            // "mb_" is available if overload is used, so use it ...
10657
            return \mb_substr_count($haystack, $needle, 'CP850'); // 8-BIT
10658
        }
10659
10660
        if ($length === null) {
10661
            return \substr_count($haystack, $needle, $offset);
10662
        }
10663
10664
        return \substr_count($haystack, $needle, $offset, $length);
10665
    }
10666
10667
    /**
10668
     * Returns the number of occurrences of $substring in the given string.
10669
     * By default, the comparison is case-sensitive, but can be made insensitive
10670
     * by setting $case_sensitive to false.
10671
     *
10672
     * @param string $str            <p>The input string.</p>
10673
     * @param string $substring      <p>The substring to search for.</p>
10674
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
10675
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
10676
     *
10677
     * @return int
10678
     */
10679
    public static function substr_count_simple(
10680
        string $str,
10681
        string $substring,
10682
        bool $case_sensitive = true,
10683
        string $encoding = 'UTF-8'
10684
    ): int {
10685 15
        if ($str === '' || $substring === '') {
10686 2
            return 0;
10687
        }
10688
10689 13
        if ($encoding === 'UTF-8') {
10690 7
            if ($case_sensitive) {
10691
                return (int) \mb_substr_count($str, $substring);
10692
            }
10693
10694 7
            return (int) \mb_substr_count(
10695 7
                \mb_strtoupper($str),
10696 7
                \mb_strtoupper($substring)
10697
            );
10698
        }
10699
10700 6
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10701
10702 6
        if ($case_sensitive) {
10703 3
            return (int) \mb_substr_count($str, $substring, $encoding);
10704
        }
10705
10706 3
        return (int) \mb_substr_count(
10707 3
            self::strtocasefold($str, true, false, $encoding, null, false),
10708 3
            self::strtocasefold($substring, true, false, $encoding, null, false),
10709 3
            $encoding
10710
        );
10711
    }
10712
10713
    /**
10714
     * Removes a prefix ($needle) from the beginning of the string ($haystack), case-insensitive.
10715
     *
10716
     * @param string $haystack <p>The string to search in.</p>
10717
     * @param string $needle   <p>The substring to search for.</p>
10718
     *
10719
     * @return string return the sub-string
10720
     */
10721
    public static function substr_ileft(string $haystack, string $needle): string
10722
    {
10723 2
        if ($haystack === '') {
10724 2
            return '';
10725
        }
10726
10727 2
        if ($needle === '') {
10728 2
            return $haystack;
10729
        }
10730
10731 2
        if (self::str_istarts_with($haystack, $needle) === true) {
10732 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
10733
        }
10734
10735 2
        return $haystack;
10736
    }
10737
10738
    /**
10739
     * Get part of a string process in bytes.
10740
     *
10741
     * @param string $str    <p>The string being checked.</p>
10742
     * @param int    $offset <p>The first position used in str.</p>
10743
     * @param int    $length [optional] <p>The maximum length of the returned string.</p>
10744
     *
10745
     * @return false|string
10746
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
10747
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
10748
     *                      characters long, <b>FALSE</b> will be returned.
10749
     */
10750
    public static function substr_in_byte(string $str, int $offset = 0, int $length = null)
10751
    {
10752
        // empty string
10753
        if ($str === '' || $length === 0) {
10754
            return '';
10755
        }
10756
10757
        // whole string
10758
        if (!$offset && $length === null) {
10759
            return $str;
10760
        }
10761
10762
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10763
            // "mb_" is available if overload is used, so use it ...
10764
            return \mb_substr($str, $offset, $length, 'CP850'); // 8-BIT
10765
        }
10766
10767
        return \substr($str, $offset, $length ?? 2147483647);
10768
    }
10769
10770
    /**
10771
     * Removes a suffix ($needle) from the end of the string ($haystack), case-insensitive.
10772
     *
10773
     * @param string $haystack <p>The string to search in.</p>
10774
     * @param string $needle   <p>The substring to search for.</p>
10775
     *
10776
     * @return string return the sub-string
10777
     */
10778
    public static function substr_iright(string $haystack, string $needle): string
10779
    {
10780 2
        if ($haystack === '') {
10781 2
            return '';
10782
        }
10783
10784 2
        if ($needle === '') {
10785 2
            return $haystack;
10786
        }
10787
10788 2
        if (self::str_iends_with($haystack, $needle) === true) {
10789 2
            $haystack = (string) \mb_substr($haystack, 0, (int) self::strlen($haystack) - (int) self::strlen($needle));
10790
        }
10791
10792 2
        return $haystack;
10793
    }
10794
10795
    /**
10796
     * Removes a prefix ($needle) from the beginning of the string ($haystack).
10797
     *
10798
     * @param string $haystack <p>The string to search in.</p>
10799
     * @param string $needle   <p>The substring to search for.</p>
10800
     *
10801
     * @return string return the sub-string
10802
     */
10803
    public static function substr_left(string $haystack, string $needle): string
10804
    {
10805 2
        if ($haystack === '') {
10806 2
            return '';
10807
        }
10808
10809 2
        if ($needle === '') {
10810 2
            return $haystack;
10811
        }
10812
10813 2
        if (self::str_starts_with($haystack, $needle) === true) {
10814 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
10815
        }
10816
10817 2
        return $haystack;
10818
    }
10819
10820
    /**
10821
     * Replace text within a portion of a string.
10822
     *
10823
     * source: https://gist.github.com/stemar/8287074
10824
     *
10825
     * @param string|string[] $str         <p>The input string or an array of stings.</p>
10826
     * @param string|string[] $replacement <p>The replacement string or an array of stings.</p>
10827
     * @param int|int[]       $offset      <p>
10828
     *                                     If start is positive, the replacing will begin at the start'th offset
10829
     *                                     into string.
10830
     *                                     <br><br>
10831
     *                                     If start is negative, the replacing will begin at the start'th character
10832
     *                                     from the end of string.
10833
     *                                     </p>
10834
     * @param int|int[]|null  $length      [optional] <p>If given and is positive, it represents the length of the
10835
     *                                     portion of string which is to be replaced. If it is negative, it
10836
     *                                     represents the number of characters from the end of string at which to
10837
     *                                     stop replacing. If it is not given, then it will default to strlen(
10838
     *                                     string ); i.e. end the replacing at the end of string. Of course, if
10839
     *                                     length is zero then this function will have the effect of inserting
10840
     *                                     replacement into string at the given start offset.</p>
10841
     * @param string          $encoding    [optional] <p>Set the charset for e.g. "mb_" function</p>
10842
     *
10843
     * @return string|string[] The result string is returned. If string is an array then array is returned.
10844
     */
10845
    public static function substr_replace(
10846
        $str,
10847
        $replacement,
10848
        $offset,
10849
        $length = null,
10850
        string $encoding = 'UTF-8'
10851
    ) {
10852 10
        if (\is_array($str) === true) {
10853 1
            $num = \count($str);
10854
10855
            // the replacement
10856 1
            if (\is_array($replacement) === true) {
10857 1
                $replacement = \array_slice($replacement, 0, $num);
10858
            } else {
10859 1
                $replacement = \array_pad([$replacement], $num, $replacement);
10860
            }
10861
10862
            // the offset
10863 1
            if (\is_array($offset) === true) {
10864 1
                $offset = \array_slice($offset, 0, $num);
10865 1
                foreach ($offset as &$value_tmp) {
10866 1
                    $value_tmp = (int) $value_tmp === $value_tmp ? $value_tmp : 0;
10867
                }
10868 1
                unset($value_tmp);
10869
            } else {
10870 1
                $offset = \array_pad([$offset], $num, $offset);
10871
            }
10872
10873
            // the length
10874 1
            if ($length === null) {
10875 1
                $length = \array_fill(0, $num, 0);
10876 1
            } elseif (\is_array($length) === true) {
10877 1
                $length = \array_slice($length, 0, $num);
10878 1
                foreach ($length as &$value_tmp_V2) {
10879 1
                    $value_tmp_V2 = (int) $value_tmp_V2 === $value_tmp_V2 ? $value_tmp_V2 : $num;
10880
                }
10881 1
                unset($value_tmp_V2);
10882
            } else {
10883 1
                $length = \array_pad([$length], $num, $length);
10884
            }
10885
10886
            // recursive call
10887 1
            return \array_map([self::class, 'substr_replace'], $str, $replacement, $offset, $length);
10888
        }
10889
10890 10
        if (\is_array($replacement) === true) {
10891 1
            if ($replacement !== []) {
10892 1
                $replacement = $replacement[0];
10893
            } else {
10894 1
                $replacement = '';
10895
            }
10896
        }
10897
10898
        // init
10899 10
        $str = (string) $str;
10900 10
        $replacement = (string) $replacement;
10901
10902 10
        if (\is_array($length) === true) {
10903
            throw new \InvalidArgumentException('Parameter "$length" can only be an array, if "$str" is also an array.');
10904
        }
10905
10906 10
        if (\is_array($offset) === true) {
10907
            throw new \InvalidArgumentException('Parameter "$offset" can only be an array, if "$str" is also an array.');
10908
        }
10909
10910 10
        if ($str === '') {
10911 1
            return $replacement;
10912
        }
10913
10914 9
        if (self::$SUPPORT['mbstring'] === true) {
10915 9
            $string_length = (int) self::strlen($str, $encoding);
10916
10917 9
            if ($offset < 0) {
10918 1
                $offset = (int) \max(0, $string_length + $offset);
10919 9
            } elseif ($offset > $string_length) {
10920 1
                $offset = $string_length;
10921
            }
10922
10923 9
            if ($length !== null && $length < 0) {
10924 1
                $length = (int) \max(0, $string_length - $offset + $length);
10925 9
            } elseif ($length === null || $length > $string_length) {
10926 4
                $length = $string_length;
10927
            }
10928
10929
            /** @noinspection AdditionOperationOnArraysInspection */
10930 9
            if (($offset + $length) > $string_length) {
10931 4
                $length = $string_length - $offset;
10932
            }
10933
10934
            /** @noinspection AdditionOperationOnArraysInspection */
10935 9
            return ((string) \mb_substr($str, 0, $offset, $encoding)) .
10936 9
                   $replacement .
10937 9
                   ((string) \mb_substr($str, $offset + $length, $string_length - $offset - $length, $encoding));
10938
        }
10939
10940
        //
10941
        // fallback for ascii only
10942
        //
10943
10944
        if (ASCII::is_ascii($str)) {
10945
            return ($length === null) ?
10946
                \substr_replace($str, $replacement, $offset) :
10947
                \substr_replace($str, $replacement, $offset, $length);
10948
        }
10949
10950
        //
10951
        // fallback via vanilla php
10952
        //
10953
10954
        \preg_match_all('/./us', $str, $str_matches);
10955
        \preg_match_all('/./us', $replacement, $replacement_matches);
10956
10957
        if ($length === null) {
10958
            $length_tmp = self::strlen($str, $encoding);
10959
            if ($length_tmp === false) {
10960
                // e.g.: non mbstring support + invalid chars
10961
                return '';
10962
            }
10963
            $length = (int) $length_tmp;
10964
        }
10965
10966
        \array_splice($str_matches[0], $offset, $length, $replacement_matches[0]);
10967
10968
        return \implode('', $str_matches[0]);
10969
    }
10970
10971
    /**
10972
     * Removes a suffix ($needle) from the end of the string ($haystack).
10973
     *
10974
     * @param string $haystack <p>The string to search in.</p>
10975
     * @param string $needle   <p>The substring to search for.</p>
10976
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
10977
     *
10978
     * @return string return the sub-string
10979
     */
10980
    public static function substr_right(
10981
        string $haystack,
10982
        string $needle,
10983
        string $encoding = 'UTF-8'
10984
    ): string {
10985 2
        if ($haystack === '') {
10986 2
            return '';
10987
        }
10988
10989 2
        if ($needle === '') {
10990 2
            return $haystack;
10991
        }
10992
10993
        if (
10994 2
            $encoding === 'UTF-8'
10995
            &&
10996 2
            \substr($haystack, -\strlen($needle)) === $needle
10997
        ) {
10998 2
            return (string) \mb_substr($haystack, 0, (int) \mb_strlen($haystack) - (int) \mb_strlen($needle));
10999
        }
11000
11001 2
        if (\substr($haystack, -\strlen($needle)) === $needle) {
11002
            return (string) self::substr(
11003
                $haystack,
11004
                0,
11005
                (int) self::strlen($haystack, $encoding) - (int) self::strlen($needle, $encoding),
11006
                $encoding
11007
            );
11008
        }
11009
11010 2
        return $haystack;
11011
    }
11012
11013
    /**
11014
     * Returns a case swapped version of the string.
11015
     *
11016
     * @param string $str        <p>The input string.</p>
11017
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11018
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11019
     *
11020
     * @return string each character's case swapped
11021
     */
11022
    public static function swapCase(string $str, string $encoding = 'UTF-8', bool $clean_utf8 = false): string
11023
    {
11024 6
        if ($str === '') {
11025 1
            return '';
11026
        }
11027
11028 6
        if ($clean_utf8 === true) {
11029
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11030
            // if invalid characters are found in $haystack before $needle
11031 2
            $str = self::clean($str);
11032
        }
11033
11034 6
        if ($encoding === 'UTF-8') {
11035 4
            return (string) (\mb_strtolower($str) ^ \mb_strtoupper($str) ^ $str);
11036
        }
11037
11038 4
        return (string) (self::strtolower($str, $encoding) ^ self::strtoupper($str, $encoding) ^ $str);
11039
    }
11040
11041
    /**
11042
     * Checks whether symfony-polyfills are used.
11043
     *
11044
     * @return bool
11045
     *              <strong>true</strong> if in use, <strong>false</strong> otherwise
11046
     */
11047
    public static function symfony_polyfill_used(): bool
11048
    {
11049
        // init
11050
        $return = false;
11051
11052
        $return_tmp = \extension_loaded('mbstring');
11053
        if ($return_tmp === false && \function_exists('mb_strlen')) {
11054
            $return = true;
11055
        }
11056
11057
        $return_tmp = \extension_loaded('iconv');
11058
        if ($return_tmp === false && \function_exists('iconv')) {
11059
            $return = true;
11060
        }
11061
11062
        return $return;
11063
    }
11064
11065
    /**
11066
     * @param string $str
11067
     * @param int    $tab_length
11068
     *
11069
     * @return string
11070
     */
11071
    public static function tabs_to_spaces(string $str, int $tab_length = 4): string
11072
    {
11073 6
        if ($tab_length === 4) {
11074 3
            $spaces = '    ';
11075 3
        } elseif ($tab_length === 2) {
11076 1
            $spaces = '  ';
11077
        } else {
11078 2
            $spaces = \str_repeat(' ', $tab_length);
11079
        }
11080
11081 6
        return \str_replace("\t", $spaces, $str);
11082
    }
11083
11084
    /**
11085
     * Converts the first character of each word in the string to uppercase
11086
     * and all other chars to lowercase.
11087
     *
11088
     * @param string      $str                           <p>The input string.</p>
11089
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
11090
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
11091
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
11092
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
11093
     *
11094
     * @return string
11095
     *                <p>A string with all characters of $str being title-cased.</p>
11096
     */
11097
    public static function titlecase(
11098
        string $str,
11099
        string $encoding = 'UTF-8',
11100
        bool $clean_utf8 = false,
11101
        string $lang = null,
11102
        bool $try_to_keep_the_string_length = false
11103
    ): string {
11104 5
        if ($clean_utf8 === true) {
11105
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11106
            // if invalid characters are found in $haystack before $needle
11107
            $str = self::clean($str);
11108
        }
11109
11110 5
        if ($lang === null && $try_to_keep_the_string_length === false) {
11111 5
            if ($encoding === 'UTF-8') {
11112 3
                return \mb_convert_case($str, \MB_CASE_TITLE);
11113
            }
11114
11115 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11116
11117 2
            return \mb_convert_case($str, \MB_CASE_TITLE, $encoding);
11118
        }
11119
11120
        return self::str_titleize(
11121
            $str,
11122
            null,
11123
            $encoding,
11124
            false,
11125
            $lang,
11126
            $try_to_keep_the_string_length,
11127
            false
11128
        );
11129
    }
11130
11131
    /**
11132
     * alias for "UTF8::to_ascii()"
11133
     *
11134
     * @param string $str
11135
     * @param string $subst_chr
11136
     * @param bool   $strict
11137
     *
11138
     * @return string
11139
     *
11140
     * @see UTF8::to_ascii()
11141
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
11142
     */
11143
    public static function toAscii(
11144
        string $str,
11145
        string $subst_chr = '?',
11146
        bool $strict = false
11147
    ): string {
11148 7
        return self::to_ascii($str, $subst_chr, $strict);
11149
    }
11150
11151
    /**
11152
     * alias for "UTF8::to_iso8859()"
11153
     *
11154
     * @param string|string[] $str
11155
     *
11156
     * @return string|string[]
11157
     *
11158
     * @see UTF8::to_iso8859()
11159
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
11160
     */
11161
    public static function toIso8859($str)
11162
    {
11163 2
        return self::to_iso8859($str);
11164
    }
11165
11166
    /**
11167
     * alias for "UTF8::to_latin1()"
11168
     *
11169
     * @param string|string[] $str
11170
     *
11171
     * @return string|string[]
11172
     *
11173
     * @see UTF8::to_iso8859()
11174
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
11175
     */
11176
    public static function toLatin1($str)
11177
    {
11178 2
        return self::to_iso8859($str);
11179
    }
11180
11181
    /**
11182
     * alias for "UTF8::to_utf8()"
11183
     *
11184
     * @param string|string[] $str
11185
     *
11186
     * @return string|string[]
11187
     *
11188
     * @see UTF8::to_utf8()
11189
     * @deprecated <p>please use "UTF8::to_utf8()"</p>
11190
     */
11191
    public static function toUTF8($str)
11192
    {
11193 2
        return self::to_utf8($str);
11194
    }
11195
11196
    /**
11197
     * Convert a string into ASCII.
11198
     *
11199
     * @param string $str     <p>The input string.</p>
11200
     * @param string $unknown [optional] <p>Character use if character unknown. (default is ?)</p>
11201
     * @param bool   $strict  [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad
11202
     *                        performance</p>
11203
     *
11204
     * @return string
11205
     */
11206
    public static function to_ascii(
11207
        string $str,
11208
        string $unknown = '?',
11209
        bool $strict = false
11210
    ): string {
11211 37
        return ASCII::to_transliterate($str, $unknown, $strict);
11212
    }
11213
11214
    /**
11215
     * @param mixed $str
11216
     *
11217
     * @return bool
11218
     */
11219
    public static function to_boolean($str): bool
11220
    {
11221
        // init
11222 19
        $str = (string) $str;
11223
11224 19
        if ($str === '') {
11225 2
            return false;
11226
        }
11227
11228
        // Info: http://php.net/manual/en/filter.filters.validate.php
11229
        $map = [
11230 17
            'true'  => true,
11231
            '1'     => true,
11232
            'on'    => true,
11233
            'yes'   => true,
11234
            'false' => false,
11235
            '0'     => false,
11236
            'off'   => false,
11237
            'no'    => false,
11238
        ];
11239
11240 17
        if (isset($map[$str])) {
11241 11
            return $map[$str];
11242
        }
11243
11244 6
        $key = \strtolower($str);
11245 6
        if (isset($map[$key])) {
11246 2
            return $map[$key];
11247
        }
11248
11249 4
        if (\is_numeric($str)) {
11250 2
            return ((float) $str + 0) > 0;
11251
        }
11252
11253 2
        return (bool) \trim($str);
11254
    }
11255
11256
    /**
11257
     * Convert given string to safe filename (and keep string case).
11258
     *
11259
     * @param string $str
11260
     * @param bool   $use_transliterate No transliteration, conversion etc. is done by default - unsafe characters are
11261
     *                                  simply replaced with hyphen.
11262
     * @param string $fallback_char
11263
     *
11264
     * @return string
11265
     */
11266
    public static function to_filename(
11267
        string $str,
11268
        bool $use_transliterate = false,
11269
        string $fallback_char = '-'
11270
    ): string {
11271 1
        return ASCII::to_filename(
11272 1
            $str,
11273 1
            $use_transliterate,
11274 1
            $fallback_char
11275
        );
11276
    }
11277
11278
    /**
11279
     * Convert a string into "ISO-8859"-encoding (Latin-1).
11280
     *
11281
     * @param string|string[] $str
11282
     *
11283
     * @return string|string[]
11284
     */
11285
    public static function to_iso8859($str)
11286
    {
11287 8
        if (\is_array($str) === true) {
11288 2
            foreach ($str as $k => &$v) {
11289 2
                $v = self::to_iso8859($v);
11290
            }
11291
11292 2
            return $str;
11293
        }
11294
11295 8
        $str = (string) $str;
11296 8
        if ($str === '') {
11297 2
            return '';
11298
        }
11299
11300 8
        return self::utf8_decode($str);
11301
    }
11302
11303
    /**
11304
     * alias for "UTF8::to_iso8859()"
11305
     *
11306
     * @param string|string[] $str
11307
     *
11308
     * @return string|string[]
11309
     *
11310
     * @see UTF8::to_iso8859()
11311
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
11312
     */
11313
    public static function to_latin1($str)
11314
    {
11315 2
        return self::to_iso8859($str);
11316
    }
11317
11318
    /**
11319
     * This function leaves UTF-8 characters alone, while converting almost all non-UTF8 to UTF8.
11320
     *
11321
     * <ul>
11322
     * <li>It decode UTF-8 codepoints and Unicode escape sequences.</li>
11323
     * <li>It assumes that the encoding of the original string is either WINDOWS-1252 or ISO-8859.</li>
11324
     * <li>WARNING: It does not remove invalid UTF-8 characters, so you maybe need to use "UTF8::clean()" for this
11325
     * case.</li>
11326
     * </ul>
11327
     *
11328
     * @param string|string[] $str                        <p>Any string or array.</p>
11329
     * @param bool            $decode_html_entity_to_utf8 <p>Set to true, if you need to decode html-entities.</p>
11330
     *
11331
     * @return string|string[] the UTF-8 encoded string
11332
     */
11333
    public static function to_utf8($str, bool $decode_html_entity_to_utf8 = false)
11334
    {
11335 41
        if (\is_array($str) === true) {
11336 4
            foreach ($str as $k => &$v) {
11337 4
                $v = self::to_utf8($v, $decode_html_entity_to_utf8);
11338
            }
11339
11340 4
            return $str;
11341
        }
11342
11343 41
        $str = (string) $str;
11344 41
        if ($str === '') {
11345 6
            return $str;
11346
        }
11347
11348 41
        $max = \strlen($str);
11349 41
        $buf = '';
11350
11351 41
        for ($i = 0; $i < $max; ++$i) {
11352 41
            $c1 = $str[$i];
11353
11354 41
            if ($c1 >= "\xC0") { // should be converted to UTF8, if it's not UTF8 already
11355
11356 37
                if ($c1 <= "\xDF") { // looks like 2 bytes UTF8
11357
11358 34
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
11359
11360 34
                    if ($c2 >= "\x80" && $c2 <= "\xBF") { // yeah, almost sure it's UTF8 already
11361 20
                        $buf .= $c1 . $c2;
11362 20
                        ++$i;
11363
                    } else { // not valid UTF8 - convert it
11364 34
                        $buf .= self::to_utf8_convert_helper($c1);
11365
                    }
11366 34
                } elseif ($c1 >= "\xE0" && $c1 <= "\xEF") { // looks like 3 bytes UTF8
11367
11368 33
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
11369 33
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
11370
11371 33
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF") { // yeah, almost sure it's UTF8 already
11372 15
                        $buf .= $c1 . $c2 . $c3;
11373 15
                        $i += 2;
11374
                    } else { // not valid UTF8 - convert it
11375 33
                        $buf .= self::to_utf8_convert_helper($c1);
11376
                    }
11377 26
                } elseif ($c1 >= "\xF0" && $c1 <= "\xF7") { // looks like 4 bytes UTF8
11378
11379 26
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
11380 26
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
11381 26
                    $c4 = $i + 3 >= $max ? "\x00" : $str[$i + 3];
11382
11383 26
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF" && $c4 >= "\x80" && $c4 <= "\xBF") { // yeah, almost sure it's UTF8 already
11384 8
                        $buf .= $c1 . $c2 . $c3 . $c4;
11385 8
                        $i += 3;
11386
                    } else { // not valid UTF8 - convert it
11387 26
                        $buf .= self::to_utf8_convert_helper($c1);
11388
                    }
11389
                } else { // doesn't look like UTF8, but should be converted
11390
11391 37
                    $buf .= self::to_utf8_convert_helper($c1);
11392
                }
11393 38
            } elseif (($c1 & "\xC0") === "\x80") { // needs conversion
11394
11395 4
                $buf .= self::to_utf8_convert_helper($c1);
11396
            } else { // it doesn't need conversion
11397
11398 38
                $buf .= $c1;
11399
            }
11400
        }
11401
11402
        // decode unicode escape sequences + unicode surrogate pairs
11403 41
        $buf = \preg_replace_callback(
11404 41
            '/\\\\u([dD][89abAB][0-9a-fA-F]{2})\\\\u([dD][cdefCDEF][\da-fA-F]{2})|\\\\u([0-9a-fA-F]{4})/',
11405
            /**
11406
             * @param array $matches
11407
             *
11408
             * @return string
11409
             */
11410
            static function (array $matches): string {
11411 12
                if (isset($matches[3])) {
11412 12
                    $cp = (int) \hexdec($matches[3]);
11413
                } else {
11414
                    // http://unicode.org/faq/utf_bom.html#utf16-4
11415
                    $cp = ((int) \hexdec($matches[1]) << 10)
11416
                          + (int) \hexdec($matches[2])
11417
                          + 0x10000
11418
                          - (0xD800 << 10)
11419
                          - 0xDC00;
11420
                }
11421
11422
                // https://github.com/php/php-src/blob/php-7.3.2/ext/standard/html.c#L471
11423
                //
11424
                // php_utf32_utf8(unsigned char *buf, unsigned k)
11425
11426 12
                if ($cp < 0x80) {
11427 8
                    return (string) self::chr($cp);
11428
                }
11429
11430 9
                if ($cp < 0xA0) {
11431
                    /** @noinspection UnnecessaryCastingInspection */
11432
                    return (string) self::chr(0xC0 | $cp >> 6) . (string) self::chr(0x80 | $cp & 0x3F);
11433
                }
11434
11435 9
                return self::decimal_to_chr($cp);
11436 41
            },
11437 41
            $buf
11438
        );
11439
11440 41
        if ($buf === null) {
11441
            return '';
11442
        }
11443
11444
        // decode UTF-8 codepoints
11445 41
        if ($decode_html_entity_to_utf8 === true) {
11446 2
            $buf = self::html_entity_decode($buf);
11447
        }
11448
11449 41
        return $buf;
11450
    }
11451
11452
    /**
11453
     * Strip whitespace or other characters from the beginning and end of a UTF-8 string.
11454
     *
11455
     * INFO: This is slower then "trim()"
11456
     *
11457
     * We can only use the original-function, if we use <= 7-Bit in the string / chars
11458
     * but the check for ASCII (7-Bit) cost more time, then we can safe here.
11459
     *
11460
     * @param string      $str   <p>The string to be trimmed</p>
11461
     * @param string|null $chars [optional] <p>Optional characters to be stripped</p>
11462
     *
11463
     * @return string the trimmed string
11464
     */
11465
    public static function trim(string $str = '', string $chars = null): string
11466
    {
11467 56
        if ($str === '') {
11468 9
            return '';
11469
        }
11470
11471 49
        if (self::$SUPPORT['mbstring'] === true) {
11472 49
            if ($chars) {
11473
                /** @noinspection PregQuoteUsageInspection */
11474 27
                $chars = \preg_quote($chars);
11475 27
                $pattern = "^[${chars}]+|[${chars}]+\$";
11476
            } else {
11477 22
                $pattern = '^[\\s]+|[\\s]+$';
11478
            }
11479
11480
            /** @noinspection PhpComposerExtensionStubsInspection */
11481 49
            return (string) \mb_ereg_replace($pattern, '', $str);
11482
        }
11483
11484 8
        if ($chars) {
11485
            $chars = \preg_quote($chars, '/');
11486
            $pattern = "^[${chars}]+|[${chars}]+\$";
11487
        } else {
11488 8
            $pattern = '^[\\s]+|[\\s]+$';
11489
        }
11490
11491 8
        return self::regex_replace($str, $pattern, '', '', '/');
11492
    }
11493
11494
    /**
11495
     * Makes string's first char uppercase.
11496
     *
11497
     * @param string      $str                           <p>The input string.</p>
11498
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
11499
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
11500
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt, tr</p>
11501
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ -> ß</p>
11502
     *
11503
     * @return string the resulting string
11504
     */
11505
    public static function ucfirst(
11506
        string $str,
11507
        string $encoding = 'UTF-8',
11508
        bool $clean_utf8 = false,
11509
        string $lang = null,
11510
        bool $try_to_keep_the_string_length = false
11511
    ): string {
11512 69
        if ($str === '') {
11513 3
            return '';
11514
        }
11515
11516 68
        if ($clean_utf8 === true) {
11517
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11518
            // if invalid characters are found in $haystack before $needle
11519 1
            $str = self::clean($str);
11520
        }
11521
11522 68
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
11523
11524 68
        if ($encoding === 'UTF-8') {
11525 22
            $str_part_two = (string) \mb_substr($str, 1);
11526
11527 22
            if ($use_mb_functions === true) {
11528 22
                $str_part_one = \mb_strtoupper(
11529 22
                    (string) \mb_substr($str, 0, 1)
11530
                );
11531
            } else {
11532
                $str_part_one = self::strtoupper(
11533
                    (string) \mb_substr($str, 0, 1),
11534
                    $encoding,
11535
                    false,
11536
                    $lang,
11537 22
                    $try_to_keep_the_string_length
11538
                );
11539
            }
11540
        } else {
11541 47
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11542
11543 47
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
11544
11545 47
            if ($use_mb_functions === true) {
11546 47
                $str_part_one = \mb_strtoupper(
11547 47
                    (string) \mb_substr($str, 0, 1, $encoding),
11548 47
                    $encoding
11549
                );
11550
            } else {
11551
                $str_part_one = self::strtoupper(
11552
                    (string) self::substr($str, 0, 1, $encoding),
11553
                    $encoding,
11554
                    false,
11555
                    $lang,
11556
                    $try_to_keep_the_string_length
11557
                );
11558
            }
11559
        }
11560
11561 68
        return $str_part_one . $str_part_two;
11562
    }
11563
11564
    /**
11565
     * alias for "UTF8::ucfirst()"
11566
     *
11567
     * @param string $str
11568
     * @param string $encoding
11569
     * @param bool   $clean_utf8
11570
     *
11571
     * @return string
11572
     *
11573
     * @see UTF8::ucfirst()
11574
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
11575
     */
11576
    public static function ucword(
11577
        string $str,
11578
        string $encoding = 'UTF-8',
11579
        bool $clean_utf8 = false
11580
    ): string {
11581 1
        return self::ucfirst($str, $encoding, $clean_utf8);
11582
    }
11583
11584
    /**
11585
     * Uppercase for all words in the string.
11586
     *
11587
     * @param string   $str        <p>The input string.</p>
11588
     * @param string[] $exceptions [optional] <p>Exclusion for some words.</p>
11589
     * @param string   $char_list  [optional] <p>Additional chars that contains to words and do not start a new
11590
     *                             word.</p>
11591
     * @param string   $encoding   [optional] <p>Set the charset.</p>
11592
     * @param bool     $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11593
     *
11594
     * @return string
11595
     */
11596
    public static function ucwords(
11597
        string $str,
11598
        array $exceptions = [],
11599
        string $char_list = '',
11600
        string $encoding = 'UTF-8',
11601
        bool $clean_utf8 = false
11602
    ): string {
11603 8
        if (!$str) {
11604 2
            return '';
11605
        }
11606
11607
        // INFO: mb_convert_case($str, MB_CASE_TITLE);
11608
        // -> MB_CASE_TITLE didn't only uppercase the first letter, it also lowercase all other letters
11609
11610 7
        if ($clean_utf8 === true) {
11611
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11612
            // if invalid characters are found in $haystack before $needle
11613 1
            $str = self::clean($str);
11614
        }
11615
11616 7
        $use_php_default_functions = !(bool) ($char_list . \implode('', $exceptions));
11617
11618
        if (
11619 7
            $use_php_default_functions === true
11620
            &&
11621 7
            ASCII::is_ascii($str) === true
11622
        ) {
11623
            return \ucwords($str);
11624
        }
11625
11626 7
        $words = self::str_to_words($str, $char_list);
11627 7
        $use_exceptions = $exceptions !== [];
11628
11629 7
        foreach ($words as &$word) {
11630 7
            if (!$word) {
11631 7
                continue;
11632
            }
11633
11634
            if (
11635 7
                $use_exceptions === false
11636
                ||
11637 7
                !\in_array($word, $exceptions, true)
11638
            ) {
11639 7
                $word = self::ucfirst($word, $encoding);
11640
            }
11641
        }
11642
11643 7
        return \implode('', $words);
11644
    }
11645
11646
    /**
11647
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
11648
     *
11649
     * e.g:
11650
     * 'test+test'                     => 'test test'
11651
     * 'D&#252;sseldorf'               => 'Düsseldorf'
11652
     * 'D%FCsseldorf'                  => 'Düsseldorf'
11653
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
11654
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
11655
     * 'Düsseldorf'                   => 'Düsseldorf'
11656
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
11657
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
11658
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
11659
     *
11660
     * @param string $str          <p>The input string.</p>
11661
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
11662
     *
11663
     * @return string
11664
     */
11665
    public static function urldecode(string $str, bool $multi_decode = true): string
11666
    {
11667 4
        if ($str === '') {
11668 3
            return '';
11669
        }
11670
11671
        if (
11672 4
            \strpos($str, '&') === false
11673
            &&
11674 4
            \strpos($str, '%') === false
11675
            &&
11676 4
            \strpos($str, '+') === false
11677
            &&
11678 4
            \strpos($str, '\u') === false
11679
        ) {
11680 3
            return self::fix_simple_utf8($str);
11681
        }
11682
11683 4
        $str = self::urldecode_unicode_helper($str);
11684
11685
        do {
11686 4
            $str_compare = $str;
11687
11688
            /**
11689
             * @psalm-suppress PossiblyInvalidArgument
11690
             */
11691 4
            $str = self::fix_simple_utf8(
11692 4
                \urldecode(
11693 4
                    self::html_entity_decode(
11694 4
                        self::to_utf8($str),
11695 4
                        \ENT_QUOTES | \ENT_HTML5
11696
                    )
11697
                )
11698
            );
11699 4
        } while ($multi_decode === true && $str_compare !== $str);
11700
11701 4
        return $str;
11702
    }
11703
11704
    /**
11705
     * Return a array with "urlencoded"-win1252 -> UTF-8
11706
     *
11707
     * @return string[]
11708
     *
11709
     * @deprecated <p>please use the "UTF8::urldecode()" function to decode a string</p>
11710
     */
11711
    public static function urldecode_fix_win1252_chars(): array
11712
    {
11713
        return [
11714 2
            '%20' => ' ',
11715
            '%21' => '!',
11716
            '%22' => '"',
11717
            '%23' => '#',
11718
            '%24' => '$',
11719
            '%25' => '%',
11720
            '%26' => '&',
11721
            '%27' => "'",
11722
            '%28' => '(',
11723
            '%29' => ')',
11724
            '%2A' => '*',
11725
            '%2B' => '+',
11726
            '%2C' => ',',
11727
            '%2D' => '-',
11728
            '%2E' => '.',
11729
            '%2F' => '/',
11730
            '%30' => '0',
11731
            '%31' => '1',
11732
            '%32' => '2',
11733
            '%33' => '3',
11734
            '%34' => '4',
11735
            '%35' => '5',
11736
            '%36' => '6',
11737
            '%37' => '7',
11738
            '%38' => '8',
11739
            '%39' => '9',
11740
            '%3A' => ':',
11741
            '%3B' => ';',
11742
            '%3C' => '<',
11743
            '%3D' => '=',
11744
            '%3E' => '>',
11745
            '%3F' => '?',
11746
            '%40' => '@',
11747
            '%41' => 'A',
11748
            '%42' => 'B',
11749
            '%43' => 'C',
11750
            '%44' => 'D',
11751
            '%45' => 'E',
11752
            '%46' => 'F',
11753
            '%47' => 'G',
11754
            '%48' => 'H',
11755
            '%49' => 'I',
11756
            '%4A' => 'J',
11757
            '%4B' => 'K',
11758
            '%4C' => 'L',
11759
            '%4D' => 'M',
11760
            '%4E' => 'N',
11761
            '%4F' => 'O',
11762
            '%50' => 'P',
11763
            '%51' => 'Q',
11764
            '%52' => 'R',
11765
            '%53' => 'S',
11766
            '%54' => 'T',
11767
            '%55' => 'U',
11768
            '%56' => 'V',
11769
            '%57' => 'W',
11770
            '%58' => 'X',
11771
            '%59' => 'Y',
11772
            '%5A' => 'Z',
11773
            '%5B' => '[',
11774
            '%5C' => '\\',
11775
            '%5D' => ']',
11776
            '%5E' => '^',
11777
            '%5F' => '_',
11778
            '%60' => '`',
11779
            '%61' => 'a',
11780
            '%62' => 'b',
11781
            '%63' => 'c',
11782
            '%64' => 'd',
11783
            '%65' => 'e',
11784
            '%66' => 'f',
11785
            '%67' => 'g',
11786
            '%68' => 'h',
11787
            '%69' => 'i',
11788
            '%6A' => 'j',
11789
            '%6B' => 'k',
11790
            '%6C' => 'l',
11791
            '%6D' => 'm',
11792
            '%6E' => 'n',
11793
            '%6F' => 'o',
11794
            '%70' => 'p',
11795
            '%71' => 'q',
11796
            '%72' => 'r',
11797
            '%73' => 's',
11798
            '%74' => 't',
11799
            '%75' => 'u',
11800
            '%76' => 'v',
11801
            '%77' => 'w',
11802
            '%78' => 'x',
11803
            '%79' => 'y',
11804
            '%7A' => 'z',
11805
            '%7B' => '{',
11806
            '%7C' => '|',
11807
            '%7D' => '}',
11808
            '%7E' => '~',
11809
            '%7F' => '',
11810
            '%80' => '`',
11811
            '%81' => '',
11812
            '%82' => '‚',
11813
            '%83' => 'ƒ',
11814
            '%84' => '„',
11815
            '%85' => '…',
11816
            '%86' => '†',
11817
            '%87' => '‡',
11818
            '%88' => 'ˆ',
11819
            '%89' => '‰',
11820
            '%8A' => 'Š',
11821
            '%8B' => '‹',
11822
            '%8C' => 'Œ',
11823
            '%8D' => '',
11824
            '%8E' => 'Ž',
11825
            '%8F' => '',
11826
            '%90' => '',
11827
            '%91' => '‘',
11828
            '%92' => '’',
11829
            '%93' => '“',
11830
            '%94' => '”',
11831
            '%95' => '•',
11832
            '%96' => '–',
11833
            '%97' => '—',
11834
            '%98' => '˜',
11835
            '%99' => '™',
11836
            '%9A' => 'š',
11837
            '%9B' => '›',
11838
            '%9C' => 'œ',
11839
            '%9D' => '',
11840
            '%9E' => 'ž',
11841
            '%9F' => 'Ÿ',
11842
            '%A0' => '',
11843
            '%A1' => '¡',
11844
            '%A2' => '¢',
11845
            '%A3' => '£',
11846
            '%A4' => '¤',
11847
            '%A5' => '¥',
11848
            '%A6' => '¦',
11849
            '%A7' => '§',
11850
            '%A8' => '¨',
11851
            '%A9' => '©',
11852
            '%AA' => 'ª',
11853
            '%AB' => '«',
11854
            '%AC' => '¬',
11855
            '%AD' => '',
11856
            '%AE' => '®',
11857
            '%AF' => '¯',
11858
            '%B0' => '°',
11859
            '%B1' => '±',
11860
            '%B2' => '²',
11861
            '%B3' => '³',
11862
            '%B4' => '´',
11863
            '%B5' => 'µ',
11864
            '%B6' => '¶',
11865
            '%B7' => '·',
11866
            '%B8' => '¸',
11867
            '%B9' => '¹',
11868
            '%BA' => 'º',
11869
            '%BB' => '»',
11870
            '%BC' => '¼',
11871
            '%BD' => '½',
11872
            '%BE' => '¾',
11873
            '%BF' => '¿',
11874
            '%C0' => 'À',
11875
            '%C1' => 'Á',
11876
            '%C2' => 'Â',
11877
            '%C3' => 'Ã',
11878
            '%C4' => 'Ä',
11879
            '%C5' => 'Å',
11880
            '%C6' => 'Æ',
11881
            '%C7' => 'Ç',
11882
            '%C8' => 'È',
11883
            '%C9' => 'É',
11884
            '%CA' => 'Ê',
11885
            '%CB' => 'Ë',
11886
            '%CC' => 'Ì',
11887
            '%CD' => 'Í',
11888
            '%CE' => 'Î',
11889
            '%CF' => 'Ï',
11890
            '%D0' => 'Ð',
11891
            '%D1' => 'Ñ',
11892
            '%D2' => 'Ò',
11893
            '%D3' => 'Ó',
11894
            '%D4' => 'Ô',
11895
            '%D5' => 'Õ',
11896
            '%D6' => 'Ö',
11897
            '%D7' => '×',
11898
            '%D8' => 'Ø',
11899
            '%D9' => 'Ù',
11900
            '%DA' => 'Ú',
11901
            '%DB' => 'Û',
11902
            '%DC' => 'Ü',
11903
            '%DD' => 'Ý',
11904
            '%DE' => 'Þ',
11905
            '%DF' => 'ß',
11906
            '%E0' => 'à',
11907
            '%E1' => 'á',
11908
            '%E2' => 'â',
11909
            '%E3' => 'ã',
11910
            '%E4' => 'ä',
11911
            '%E5' => 'å',
11912
            '%E6' => 'æ',
11913
            '%E7' => 'ç',
11914
            '%E8' => 'è',
11915
            '%E9' => 'é',
11916
            '%EA' => 'ê',
11917
            '%EB' => 'ë',
11918
            '%EC' => 'ì',
11919
            '%ED' => 'í',
11920
            '%EE' => 'î',
11921
            '%EF' => 'ï',
11922
            '%F0' => 'ð',
11923
            '%F1' => 'ñ',
11924
            '%F2' => 'ò',
11925
            '%F3' => 'ó',
11926
            '%F4' => 'ô',
11927
            '%F5' => 'õ',
11928
            '%F6' => 'ö',
11929
            '%F7' => '÷',
11930
            '%F8' => 'ø',
11931
            '%F9' => 'ù',
11932
            '%FA' => 'ú',
11933
            '%FB' => 'û',
11934
            '%FC' => 'ü',
11935
            '%FD' => 'ý',
11936
            '%FE' => 'þ',
11937
            '%FF' => 'ÿ',
11938
        ];
11939
    }
11940
11941
    /**
11942
     * Decodes a UTF-8 string to ISO-8859-1.
11943
     *
11944
     * @param string $str             <p>The input string.</p>
11945
     * @param bool   $keep_utf8_chars
11946
     *
11947
     * @return string
11948
     */
11949
    public static function utf8_decode(string $str, bool $keep_utf8_chars = false): string
11950
    {
11951 14
        if ($str === '') {
11952 6
            return '';
11953
        }
11954
11955
        // save for later comparision
11956 14
        $str_backup = $str;
11957 14
        $len = \strlen($str);
11958
11959 14
        if (self::$ORD === null) {
11960
            self::$ORD = self::getData('ord');
11961
        }
11962
11963 14
        if (self::$CHR === null) {
11964
            self::$CHR = self::getData('chr');
11965
        }
11966
11967 14
        $no_char_found = '?';
11968
        /** @noinspection ForeachInvariantsInspection */
11969 14
        for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
11970 14
            switch ($str[$i] & "\xF0") {
11971 14
                case "\xC0":
11972 13
                case "\xD0":
11973 13
                    $c = (self::$ORD[$str[$i] & "\x1F"] << 6) | self::$ORD[$str[++$i] & "\x3F"];
11974 13
                    $str[$j] = $c < 256 ? self::$CHR[$c] : $no_char_found;
11975
11976 13
                    break;
11977
11978
                /** @noinspection PhpMissingBreakStatementInspection */
11979 13
                case "\xF0":
11980
                    ++$i;
11981
11982
                // no break
11983
11984 13
                case "\xE0":
11985 11
                    $str[$j] = $no_char_found;
11986 11
                    $i += 2;
11987
11988 11
                    break;
11989
11990
                default:
11991 12
                    $str[$j] = $str[$i];
11992
            }
11993
        }
11994
11995
        /** @var false|string $return - needed for PhpStan (stubs error) */
11996 14
        $return = \substr($str, 0, $j);
11997 14
        if ($return === false) {
11998
            $return = '';
11999
        }
12000
12001
        if (
12002 14
            $keep_utf8_chars === true
12003
            &&
12004 14
            (int) self::strlen($return) >= (int) self::strlen($str_backup)
12005
        ) {
12006 2
            return $str_backup;
12007
        }
12008
12009 14
        return $return;
12010
    }
12011
12012
    /**
12013
     * Encodes an ISO-8859-1 string to UTF-8.
12014
     *
12015
     * @param string $str <p>The input string.</p>
12016
     *
12017
     * @return string
12018
     */
12019
    public static function utf8_encode(string $str): string
12020
    {
12021 14
        if ($str === '') {
12022 14
            return '';
12023
        }
12024
12025
        /** @var false|string $str - the polyfill maybe return false */
12026 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

12026
        $str = \utf8_encode(/** @scrutinizer ignore-type */ $str);
Loading history...
12027
12028
        /** @noinspection CallableParameterUseCaseInTypeContextInspection */
12029
        /** @psalm-suppress TypeDoesNotContainType */
12030 14
        if ($str === false) {
12031
            return '';
12032
        }
12033
12034 14
        return $str;
12035
    }
12036
12037
    /**
12038
     * fix -> utf8-win1252 chars
12039
     *
12040
     * @param string $str <p>The input string.</p>
12041
     *
12042
     * @return string
12043
     *
12044
     * @deprecated <p>please use "UTF8::fix_simple_utf8()"</p>
12045
     */
12046
    public static function utf8_fix_win1252_chars(string $str): string
12047
    {
12048 2
        return self::fix_simple_utf8($str);
12049
    }
12050
12051
    /**
12052
     * Returns an array with all utf8 whitespace characters.
12053
     *
12054
     * @see http://www.bogofilter.org/pipermail/bogofilter/2003-March/001889.html
12055
     *
12056
     * @return string[]
12057
     *                  An array with all known whitespace characters as values and the type of whitespace as keys
12058
     *                  as defined in above URL
12059
     */
12060
    public static function whitespace_table(): array
12061
    {
12062 2
        return self::$WHITESPACE_TABLE;
12063
    }
12064
12065
    /**
12066
     * Limit the number of words in a string.
12067
     *
12068
     * @param string $str        <p>The input string.</p>
12069
     * @param int    $limit      <p>The limit of words as integer.</p>
12070
     * @param string $str_add_on <p>Replacement for the striped string.</p>
12071
     *
12072
     * @return string
12073
     */
12074
    public static function words_limit(
12075
        string $str,
12076
        int $limit = 100,
12077
        string $str_add_on = '…'
12078
    ): string {
12079 2
        if ($str === '' || $limit < 1) {
12080 2
            return '';
12081
        }
12082
12083 2
        \preg_match('/^\\s*+(?:[^\\s]++\\s*+){1,' . $limit . '}/u', $str, $matches);
12084
12085
        if (
12086 2
            !isset($matches[0])
12087
            ||
12088 2
            \mb_strlen($str) === (int) \mb_strlen($matches[0])
12089
        ) {
12090 2
            return $str;
12091
        }
12092
12093 2
        return \rtrim($matches[0]) . $str_add_on;
12094
    }
12095
12096
    /**
12097
     * Wraps a string to a given number of characters
12098
     *
12099
     * @see http://php.net/manual/en/function.wordwrap.php
12100
     *
12101
     * @param string $str   <p>The input string.</p>
12102
     * @param int    $width [optional] <p>The column width.</p>
12103
     * @param string $break [optional] <p>The line is broken using the optional break parameter.</p>
12104
     * @param bool   $cut   [optional] <p>
12105
     *                      If the cut is set to true, the string is
12106
     *                      always wrapped at or before the specified width. So if you have
12107
     *                      a word that is larger than the given width, it is broken apart.
12108
     *                      </p>
12109
     *
12110
     * @return string
12111
     *                <p>The given string wrapped at the specified column.</p>
12112
     */
12113
    public static function wordwrap(
12114
        string $str,
12115
        int $width = 75,
12116
        string $break = "\n",
12117
        bool $cut = false
12118
    ): string {
12119 12
        if ($str === '' || $break === '') {
12120 4
            return '';
12121
        }
12122
12123 10
        $str_split = \explode($break, $str);
12124 10
        if ($str_split === false) {
12125
            return '';
12126
        }
12127
12128 10
        $chars = [];
12129 10
        $word_split = '';
12130 10
        foreach ($str_split as $i => $i_value) {
12131 10
            if ($i) {
12132 3
                $chars[] = $break;
12133 3
                $word_split .= '#';
12134
            }
12135
12136 10
            foreach (self::str_split($i_value) as $c) {
12137 10
                $chars[] = $c;
12138 10
                if ($c === ' ') {
12139 3
                    $word_split .= ' ';
12140
                } else {
12141 10
                    $word_split .= '?';
12142
                }
12143
            }
12144
        }
12145
12146 10
        $str_return = '';
12147 10
        $j = 0;
12148 10
        $b = -1;
12149 10
        $i = -1;
12150 10
        $word_split = \wordwrap($word_split, $width, '#', $cut);
12151
12152 10
        $max = \mb_strlen($word_split);
12153 10
        while (($b = \mb_strpos($word_split, '#', $b + 1)) !== false) {
12154 8
            for (++$i; $i < $b; ++$i) {
12155 8
                $str_return .= $chars[$j];
12156 8
                unset($chars[$j++]);
12157
12158
                // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
12159 8
                if ($i > $max) {
12160
                    break 2;
12161
                }
12162
            }
12163
12164
            if (
12165 8
                $break === $chars[$j]
12166
                ||
12167 8
                $chars[$j] === ' '
12168
            ) {
12169 5
                unset($chars[$j++]);
12170
            }
12171
12172 8
            $str_return .= $break;
12173
12174
            // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
12175 8
            if ($b > $max) {
12176
                break;
12177
            }
12178
        }
12179
12180 10
        return $str_return . \implode('', $chars);
12181
    }
12182
12183
    /**
12184
     * Line-Wrap the string after $limit, but split the string by "$delimiter" before ...
12185
     *    ... so that we wrap the per line.
12186
     *
12187
     * @param string      $str             <p>The input string.</p>
12188
     * @param int         $width           [optional] <p>The column width.</p>
12189
     * @param string      $break           [optional] <p>The line is broken using the optional break parameter.</p>
12190
     * @param bool        $cut             [optional] <p>
12191
     *                                     If the cut is set to true, the string is
12192
     *                                     always wrapped at or before the specified width. So if you have
12193
     *                                     a word that is larger than the given width, it is broken apart.
12194
     *                                     </p>
12195
     * @param bool        $add_final_break [optional] <p>
12196
     *                                     If this flag is true, then the method will add a $break at the end
12197
     *                                     of the result string.
12198
     *                                     </p>
12199
     * @param string|null $delimiter       [optional] <p>
12200
     *                                     You can change the default behavior, where we split the string by newline.
12201
     *                                     </p>
12202
     *
12203
     * @return string
12204
     */
12205
    public static function wordwrap_per_line(
12206
        string $str,
12207
        int $width = 75,
12208
        string $break = "\n",
12209
        bool $cut = false,
12210
        bool $add_final_break = true,
12211
        string $delimiter = null
12212
    ): string {
12213 1
        if ($delimiter === null) {
12214 1
            $strings = \preg_split('/\\r\\n|\\r|\\n/', $str);
12215
        } else {
12216 1
            $strings = \explode($delimiter, $str);
12217
        }
12218
12219 1
        $string_helper_array = [];
12220 1
        if ($strings !== false) {
12221 1
            foreach ($strings as $value) {
12222 1
                $string_helper_array[] = self::wordwrap($value, $width, $break, $cut);
12223
            }
12224
        }
12225
12226 1
        if ($add_final_break) {
12227 1
            $final_break = $break;
12228
        } else {
12229 1
            $final_break = '';
12230
        }
12231
12232 1
        return \implode($delimiter ?? "\n", $string_helper_array) . $final_break;
12233
    }
12234
12235
    /**
12236
     * Returns an array of Unicode White Space characters.
12237
     *
12238
     * @return string[] an array with numeric code point as key and White Space Character as value
12239
     */
12240
    public static function ws(): array
12241
    {
12242 2
        return self::$WHITESPACE;
12243
    }
12244
12245
    /**
12246
     * @param string $str
12247
     * @param string $encoding
12248
     *
12249
     * @return string
12250
     *
12251
     * @noinspection ReturnTypeCanBeDeclaredInspection
12252
     */
12253
    private static function html_entity_decode_helper(string $str, string $encoding)
12254
    {
12255
        return (string) \preg_replace_callback(
12256
            "/&#\d{2,6};/",
12257
            /**
12258
             * @param string[] $matches
12259
             *
12260
             * @return string
12261
             */
12262
            static function (array $matches) use ($encoding): string {
12263
                $return_tmp = \mb_convert_encoding($matches[0], $encoding, 'HTML-ENTITIES');
12264
                if ($return_tmp !== '"' && $return_tmp !== "'") {
12265
                    return $return_tmp;
12266
                }
12267
12268
                return $matches[0];
12269
            },
12270
            $str
12271
        );
12272
    }
12273
12274
    /**
12275
     * Checks whether the passed string contains only byte sequences that are valid UTF-8 characters.
12276
     *
12277
     * @see http://hsivonen.iki.fi/php-utf8/
12278
     *
12279
     * @param string $str    <p>The string to be checked.</p>
12280
     * @param bool   $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
12281
     *
12282
     * @return bool
12283
     *
12284
     * @noinspection ReturnTypeCanBeDeclaredInspection
12285
     */
12286
    private static function is_utf8_string(string $str, bool $strict = false)
12287
    {
12288 108
        if ($str === '') {
12289 14
            return true;
12290
        }
12291
12292 102
        if ($strict === true) {
12293 2
            $is_binary = self::is_binary($str, true);
12294
12295 2
            if ($is_binary && self::is_utf16($str, false) !== false) {
12296 2
                return false;
12297
            }
12298
12299
            if ($is_binary && self::is_utf32($str, false) !== false) {
12300
                return false;
12301
            }
12302
        }
12303
12304 102
        if (self::pcre_utf8_support() !== true) {
12305
            // If even just the first character can be matched, when the /u
12306
            // modifier is used, then it's valid UTF-8. If the UTF-8 is somehow
12307
            // invalid, nothing at all will match, even if the string contains
12308
            // some valid sequences
12309
            return \preg_match('/^./us', $str, $ar) === 1;
12310
        }
12311
12312 102
        $mState = 0; // cached expected number of octets after the current octet
12313
        // until the beginning of the next UTF8 character sequence
12314 102
        $mUcs4 = 0; // cached Unicode character
12315 102
        $mBytes = 1; // cached expected number of octets in the current sequence
12316
12317 102
        if (self::$ORD === null) {
12318
            self::$ORD = self::getData('ord');
12319
        }
12320
12321 102
        $len = \strlen($str);
12322
        /** @noinspection ForeachInvariantsInspection */
12323 102
        for ($i = 0; $i < $len; ++$i) {
12324 102
            $in = self::$ORD[$str[$i]];
12325
12326 102
            if ($mState === 0) {
12327
                // When mState is zero we expect either a US-ASCII character or a
12328
                // multi-octet sequence.
12329 102
                if ((0x80 & $in) === 0) {
12330
                    // US-ASCII, pass straight through.
12331 97
                    $mBytes = 1;
12332 83
                } elseif ((0xE0 & $in) === 0xC0) {
12333
                    // First octet of 2 octet sequence.
12334 73
                    $mUcs4 = $in;
12335 73
                    $mUcs4 = ($mUcs4 & 0x1F) << 6;
12336 73
                    $mState = 1;
12337 73
                    $mBytes = 2;
12338 58
                } elseif ((0xF0 & $in) === 0xE0) {
12339
                    // First octet of 3 octet sequence.
12340 42
                    $mUcs4 = $in;
12341 42
                    $mUcs4 = ($mUcs4 & 0x0F) << 12;
12342 42
                    $mState = 2;
12343 42
                    $mBytes = 3;
12344 29
                } elseif ((0xF8 & $in) === 0xF0) {
12345
                    // First octet of 4 octet sequence.
12346 18
                    $mUcs4 = $in;
12347 18
                    $mUcs4 = ($mUcs4 & 0x07) << 18;
12348 18
                    $mState = 3;
12349 18
                    $mBytes = 4;
12350 13
                } elseif ((0xFC & $in) === 0xF8) {
12351
                    /* First octet of 5 octet sequence.
12352
                     *
12353
                     * This is illegal because the encoded codepoint must be either
12354
                     * (a) not the shortest form or
12355
                     * (b) outside the Unicode range of 0-0x10FFFF.
12356
                     * Rather than trying to resynchronize, we will carry on until the end
12357
                     * of the sequence and let the later error handling code catch it.
12358
                     */
12359 5
                    $mUcs4 = $in;
12360 5
                    $mUcs4 = ($mUcs4 & 0x03) << 24;
12361 5
                    $mState = 4;
12362 5
                    $mBytes = 5;
12363 10
                } elseif ((0xFE & $in) === 0xFC) {
12364
                    // First octet of 6 octet sequence, see comments for 5 octet sequence.
12365 5
                    $mUcs4 = $in;
12366 5
                    $mUcs4 = ($mUcs4 & 1) << 30;
12367 5
                    $mState = 5;
12368 5
                    $mBytes = 6;
12369
                } else {
12370
                    // Current octet is neither in the US-ASCII range nor a legal first
12371
                    // octet of a multi-octet sequence.
12372 102
                    return false;
12373
                }
12374 83
            } elseif ((0xC0 & $in) === 0x80) {
12375
12376
                // When mState is non-zero, we expect a continuation of the multi-octet
12377
                // sequence
12378
12379
                // Legal continuation.
12380 75
                $shift = ($mState - 1) * 6;
12381 75
                $tmp = $in;
12382 75
                $tmp = ($tmp & 0x0000003F) << $shift;
12383 75
                $mUcs4 |= $tmp;
12384
                // Prefix: End of the multi-octet sequence. mUcs4 now contains the final
12385
                // Unicode code point to be output.
12386 75
                if (--$mState === 0) {
12387
                    // Check for illegal sequences and code points.
12388
                    //
12389
                    // From Unicode 3.1, non-shortest form is illegal
12390
                    if (
12391 75
                        ($mBytes === 2 && $mUcs4 < 0x0080)
12392
                        ||
12393 75
                        ($mBytes === 3 && $mUcs4 < 0x0800)
12394
                        ||
12395 75
                        ($mBytes === 4 && $mUcs4 < 0x10000)
12396
                        ||
12397 75
                        ($mBytes > 4)
12398
                        ||
12399
                        // From Unicode 3.2, surrogate characters are illegal.
12400 75
                        (($mUcs4 & 0xFFFFF800) === 0xD800)
12401
                        ||
12402
                        // Code points outside the Unicode range are illegal.
12403 75
                        ($mUcs4 > 0x10FFFF)
12404
                    ) {
12405 8
                        return false;
12406
                    }
12407
                    // initialize UTF8 cache
12408 75
                    $mState = 0;
12409 75
                    $mUcs4 = 0;
12410 75
                    $mBytes = 1;
12411
                }
12412
            } else {
12413
                // ((0xC0 & (*in) != 0x80) && (mState != 0))
12414
                // Incomplete multi-octet sequence.
12415 35
                return false;
12416
            }
12417
        }
12418
12419 67
        return true;
12420
    }
12421
12422
    /**
12423
     * @param string $str
12424
     * @param bool   $use_lowercase      <p>Use uppercase by default, otherwise use lowercase.</p>
12425
     * @param bool   $use_full_case_fold <p>Convert not only common cases.</p>
12426
     *
12427
     * @return string
12428
     *
12429
     * @noinspection ReturnTypeCanBeDeclaredInspection
12430
     */
12431
    private static function fixStrCaseHelper(
12432
        string $str,
12433
        $use_lowercase = false,
12434
        $use_full_case_fold = false
12435
    ) {
12436 33
        $upper = self::$COMMON_CASE_FOLD['upper'];
12437 33
        $lower = self::$COMMON_CASE_FOLD['lower'];
12438
12439 33
        if ($use_lowercase === true) {
12440 2
            $str = \str_replace(
12441 2
                $upper,
12442 2
                $lower,
12443 2
                $str
12444
            );
12445
        } else {
12446 31
            $str = \str_replace(
12447 31
                $lower,
12448 31
                $upper,
12449 31
                $str
12450
            );
12451
        }
12452
12453 33
        if ($use_full_case_fold) {
12454 31
            static $FULL_CASE_FOLD = null;
12455 31
            if ($FULL_CASE_FOLD === null) {
12456 1
                $FULL_CASE_FOLD = self::getData('caseFolding_full');
12457
            }
12458
12459 31
            if ($use_lowercase === true) {
12460 2
                $str = \str_replace($FULL_CASE_FOLD[0], $FULL_CASE_FOLD[1], $str);
12461
            } else {
12462 29
                $str = \str_replace($FULL_CASE_FOLD[1], $FULL_CASE_FOLD[0], $str);
12463
            }
12464
        }
12465
12466 33
        return $str;
12467
    }
12468
12469
    /**
12470
     * get data from "/data/*.php"
12471
     *
12472
     * @param string $file
12473
     *
12474
     * @return array
12475
     *
12476
     * @noinspection ReturnTypeCanBeDeclaredInspection
12477
     */
12478
    private static function getData(string $file)
12479
    {
12480
        /** @noinspection PhpIncludeInspection */
12481
        /** @noinspection UsingInclusionReturnValueInspection */
12482
        /** @psalm-suppress UnresolvableInclude */
12483 6
        return include __DIR__ . '/data/' . $file . '.php';
12484
    }
12485
12486
    /**
12487
     * @return true|null
12488
     */
12489
    private static function initEmojiData()
12490
    {
12491 12
        if (self::$EMOJI_KEYS_CACHE === null) {
12492 1
            if (self::$EMOJI === null) {
12493 1
                self::$EMOJI = self::getData('emoji');
12494
            }
12495
12496 1
            \uksort(
12497 1
                self::$EMOJI,
12498
                static function (string $a, string $b): int {
12499 1
                    return \strlen($b) <=> \strlen($a);
12500 1
                }
12501
            );
12502
12503 1
            self::$EMOJI_KEYS_CACHE = \array_keys(self::$EMOJI);
12504 1
            self::$EMOJI_VALUES_CACHE = \array_values(self::$EMOJI);
12505
12506 1
            foreach (self::$EMOJI_KEYS_CACHE as $key) {
12507 1
                $tmp_key = \crc32($key);
12508 1
                self::$EMOJI_KEYS_REVERSIBLE_CACHE[] = '_-_PORTABLE_UTF8_-_' . $tmp_key . '_-_' . \strrev((string) $tmp_key) . '_-_8FTU_ELBATROP_-_';
12509
            }
12510
12511 1
            return true;
12512
        }
12513
12514 12
        return null;
12515
    }
12516
12517
    /**
12518
     * Checks whether mbstring "overloaded" is active on the server.
12519
     *
12520
     * @return bool
12521
     *
12522
     * @noinspection ReturnTypeCanBeDeclaredInspection
12523
     */
12524
    private static function mbstring_overloaded()
12525
    {
12526
        /**
12527
         * INI directive 'mbstring.func_overload' is deprecated since PHP 7.2
12528
         */
12529
12530
        /** @noinspection PhpComposerExtensionStubsInspection */
12531
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
12532
        return \defined('MB_OVERLOAD_STRING')
12533
               &&
12534
               ((int) @\ini_get('mbstring.func_overload') & \MB_OVERLOAD_STRING);
12535
    }
12536
12537
    /**
12538
     * @param array    $strings
12539
     * @param bool     $remove_empty_values
12540
     * @param int|null $remove_short_values
12541
     *
12542
     * @return array
12543
     *
12544
     * @noinspection ReturnTypeCanBeDeclaredInspection
12545
     */
12546
    private static function reduce_string_array(
12547
        array $strings,
12548
        bool $remove_empty_values,
12549
        int $remove_short_values = null
12550
    ) {
12551
        // init
12552 2
        $return = [];
12553
12554 2
        foreach ($strings as &$str) {
12555
            if (
12556 2
                $remove_short_values !== null
12557
                &&
12558 2
                \mb_strlen($str) <= $remove_short_values
12559
            ) {
12560 2
                continue;
12561
            }
12562
12563
            if (
12564 2
                $remove_empty_values === true
12565
                &&
12566 2
                \trim($str) === ''
12567
            ) {
12568 2
                continue;
12569
            }
12570
12571 2
            $return[] = $str;
12572
        }
12573
12574 2
        return $return;
12575
    }
12576
12577
    /**
12578
     * rxClass
12579
     *
12580
     * @param string $s
12581
     * @param string $class
12582
     *
12583
     * @return string
12584
     *
12585
     * @noinspection ReturnTypeCanBeDeclaredInspection
12586
     */
12587
    private static function rxClass(string $s, string $class = '')
12588
    {
12589 33
        static $RX_CLASS_CACHE = [];
12590
12591 33
        $cache_key = $s . $class;
12592
12593 33
        if (isset($RX_CLASS_CACHE[$cache_key])) {
12594 21
            return $RX_CLASS_CACHE[$cache_key];
12595
        }
12596
12597 16
        $class_array = [$class];
12598
12599
        /** @noinspection SuspiciousLoopInspection */
12600
        /** @noinspection AlterInForeachInspection */
12601 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...
12602 15
            if ($s === '-') {
12603
                $class_array[0] = '-' . $class_array[0];
12604 15
            } elseif (!isset($s[2])) {
12605 15
                $class_array[0] .= \preg_quote($s, '/');
12606 1
            } elseif (self::strlen($s) === 1) {
12607 1
                $class_array[0] .= $s;
12608
            } else {
12609 15
                $class_array[] = $s;
12610
            }
12611
        }
12612
12613 16
        if ($class_array[0]) {
12614 16
            $class_array[0] = '[' . $class_array[0] . ']';
12615
        }
12616
12617 16
        if (\count($class_array) === 1) {
12618 16
            $return = $class_array[0];
12619
        } else {
12620
            $return = '(?:' . \implode('|', $class_array) . ')';
12621
        }
12622
12623 16
        $RX_CLASS_CACHE[$cache_key] = $return;
12624
12625 16
        return $return;
12626
    }
12627
12628
    /**
12629
     * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
12630
     *
12631
     * @param string $names
12632
     * @param string $delimiter
12633
     * @param string $encoding
12634
     *
12635
     * @return string
12636
     *
12637
     * @noinspection ReturnTypeCanBeDeclaredInspection
12638
     */
12639
    private static function str_capitalize_name_helper(
12640
        string $names,
12641
        string $delimiter,
12642
        string $encoding = 'UTF-8'
12643
    ) {
12644
        // init
12645 1
        $name_helper_array = \explode($delimiter, $names);
12646 1
        if ($name_helper_array === false) {
12647
            return '';
12648
        }
12649
12650
        $special_cases = [
12651 1
            'names' => [
12652
                'ab',
12653
                'af',
12654
                'al',
12655
                'and',
12656
                'ap',
12657
                'bint',
12658
                'binte',
12659
                'da',
12660
                'de',
12661
                'del',
12662
                'den',
12663
                'der',
12664
                'di',
12665
                'dit',
12666
                'ibn',
12667
                'la',
12668
                'mac',
12669
                'nic',
12670
                'of',
12671
                'ter',
12672
                'the',
12673
                'und',
12674
                'van',
12675
                'von',
12676
                'y',
12677
                'zu',
12678
            ],
12679
            'prefixes' => [
12680
                'al-',
12681
                "d'",
12682
                'ff',
12683
                "l'",
12684
                'mac',
12685
                'mc',
12686
                'nic',
12687
            ],
12688
        ];
12689
12690 1
        foreach ($name_helper_array as &$name) {
12691 1
            if (\in_array($name, $special_cases['names'], true)) {
12692 1
                continue;
12693
            }
12694
12695 1
            $continue = false;
12696
12697 1
            if ($delimiter === '-') {
12698
                /** @noinspection AlterInForeachInspection */
12699 1
                foreach ((array) $special_cases['names'] as &$beginning) {
12700 1
                    if (self::strpos($name, $beginning, 0, $encoding) === 0) {
12701 1
                        $continue = true;
12702
                    }
12703
                }
12704
            }
12705
12706
            /** @noinspection AlterInForeachInspection */
12707 1
            foreach ((array) $special_cases['prefixes'] as &$beginning) {
12708 1
                if (self::strpos($name, $beginning, 0, $encoding) === 0) {
12709 1
                    $continue = true;
12710
                }
12711
            }
12712
12713 1
            if ($continue === true) {
12714 1
                continue;
12715
            }
12716
12717 1
            $name = self::ucfirst($name);
12718
        }
12719
12720 1
        return \implode($delimiter, $name_helper_array);
12721
    }
12722
12723
    /**
12724
     * Generic case-sensitive transformation for collation matching.
12725
     *
12726
     * @param string $str <p>The input string</p>
12727
     *
12728
     * @return string|null
12729
     */
12730
    private static function strtonatfold(string $str)
12731
    {
12732
        /** @noinspection PhpUndefinedClassInspection */
12733 6
        return \preg_replace(
12734 6
            '/\p{Mn}+/u',
12735 6
            '',
12736 6
            \Normalizer::normalize($str, \Normalizer::NFD)
12737
        );
12738
    }
12739
12740
    /**
12741
     * @param int|string $input
12742
     *
12743
     * @return string
12744
     *
12745
     * @noinspection ReturnTypeCanBeDeclaredInspection
12746
     */
12747
    private static function to_utf8_convert_helper($input)
12748
    {
12749
        // init
12750 31
        $buf = '';
12751
12752 31
        if (self::$ORD === null) {
12753 1
            self::$ORD = self::getData('ord');
12754
        }
12755
12756 31
        if (self::$CHR === null) {
12757 1
            self::$CHR = self::getData('chr');
12758
        }
12759
12760 31
        if (self::$WIN1252_TO_UTF8 === null) {
12761 1
            self::$WIN1252_TO_UTF8 = self::getData('win1252_to_utf8');
12762
        }
12763
12764 31
        $ordC1 = self::$ORD[$input];
12765 31
        if (isset(self::$WIN1252_TO_UTF8[$ordC1])) { // found in Windows-1252 special cases
12766 31
            $buf .= self::$WIN1252_TO_UTF8[$ordC1];
12767
        } else {
12768
            /** @noinspection OffsetOperationsInspection */
12769 1
            $cc1 = self::$CHR[$ordC1 / 64] | "\xC0";
12770 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...
12771 1
            $buf .= $cc1 . $cc2;
12772
        }
12773
12774 31
        return $buf;
12775
    }
12776
12777
    /**
12778
     * @param string $str
12779
     *
12780
     * @return string
12781
     *
12782
     * @noinspection ReturnTypeCanBeDeclaredInspection
12783
     */
12784
    private static function urldecode_unicode_helper(string $str)
12785
    {
12786 9
        $pattern = '/%u([0-9a-fA-F]{3,4})/';
12787 9
        if (\preg_match($pattern, $str)) {
12788 7
            $str = (string) \preg_replace($pattern, '&#x\\1;', $str);
12789
        }
12790
12791 9
        return $str;
12792
    }
12793
}
12794