Passed
Push — master ( c2c4e0...c304ca )
by Lars
13:13 queued 02:56
created

UTF8::iconv_loaded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

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

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

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