Passed
Push — master ( f0bb21...e70be3 )
by Lars
03:40
created

UTF8::strpos()   F

Complexity

Conditions 27
Paths 907

Size

Total Lines 131
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 31.5191

Importance

Changes 11
Bugs 7 Features 2
Metric Value
cc 27
eloc 58
c 11
b 7
f 2
nc 907
nop 5
dl 0
loc 131
ccs 40
cts 49
cp 0.8163
crap 31.5191
rs 0.1291

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

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

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