Passed
Push — master ( d625a3...9ba13f )
by Lars
09:21
created

UTF8::ord()   C

Complexity

Conditions 16
Paths 146

Size

Total Lines 77
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 16.351

Importance

Changes 10
Bugs 6 Features 0
Metric Value
cc 16
eloc 26
c 10
b 6
f 0
nc 146
nop 2
dl 0
loc 77
ccs 24
cts 27
cp 0.8889
crap 16.351
rs 5.1833

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * @psalm-immutable
9
 */
10
final class UTF8
11
{
12
    /**
13
     * (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control])
14
     * This regular expression is a work around for http://bugs.exim.org/1279
15
     *
16
     * @deprecated <p>please don't use it anymore</p>
17
     */
18
    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}])";
19
20
    /**
21
     * Bom => Byte-Length
22
     *
23
     * INFO: https://en.wikipedia.org/wiki/Byte_order_mark
24
     *
25
     * @var array<string, int>
26
     */
27
    private static $BOM = [
28
        "\xef\xbb\xbf"     => 3, // UTF-8 BOM
29
        ''              => 6, // UTF-8 BOM as "WINDOWS-1252" (one char has [maybe] more then one byte ...)
30
        "\x00\x00\xfe\xff" => 4, // UTF-32 (BE) BOM
31
        '  þÿ'             => 6, // UTF-32 (BE) BOM as "WINDOWS-1252"
32
        "\xff\xfe\x00\x00" => 4, // UTF-32 (LE) BOM
33
        'ÿþ  '             => 6, // UTF-32 (LE) BOM as "WINDOWS-1252"
34
        "\xfe\xff"         => 2, // UTF-16 (BE) BOM
35
        'þÿ'               => 4, // UTF-16 (BE) BOM as "WINDOWS-1252"
36
        "\xff\xfe"         => 2, // UTF-16 (LE) BOM
37
        'ÿþ'               => 4, // UTF-16 (LE) BOM as "WINDOWS-1252"
38
    ];
39
40
    /**
41
     * Numeric code point => UTF-8 Character
42
     *
43
     * url: http://www.w3schools.com/charsets/ref_utf_punctuation.asp
44
     *
45
     * @var array<int, string>
46
     */
47
    private static $WHITESPACE = [
48
        // NULL Byte
49
        0 => "\x0",
50
        // Tab
51
        9 => "\x9",
52
        // New Line
53
        10 => "\xa",
54
        // Vertical Tab
55
        11 => "\xb",
56
        // Carriage Return
57
        13 => "\xd",
58
        // Ordinary Space
59
        32 => "\x20",
60
        // NO-BREAK SPACE
61
        160 => "\xc2\xa0",
62
        // OGHAM SPACE MARK
63
        5760 => "\xe1\x9a\x80",
64
        // MONGOLIAN VOWEL SEPARATOR
65
        6158 => "\xe1\xa0\x8e",
66
        // EN QUAD
67
        8192 => "\xe2\x80\x80",
68
        // EM QUAD
69
        8193 => "\xe2\x80\x81",
70
        // EN SPACE
71
        8194 => "\xe2\x80\x82",
72
        // EM SPACE
73
        8195 => "\xe2\x80\x83",
74
        // THREE-PER-EM SPACE
75
        8196 => "\xe2\x80\x84",
76
        // FOUR-PER-EM SPACE
77
        8197 => "\xe2\x80\x85",
78
        // SIX-PER-EM SPACE
79
        8198 => "\xe2\x80\x86",
80
        // FIGURE SPACE
81
        8199 => "\xe2\x80\x87",
82
        // PUNCTUATION SPACE
83
        8200 => "\xe2\x80\x88",
84
        // THIN SPACE
85
        8201 => "\xe2\x80\x89",
86
        // HAIR SPACE
87
        8202 => "\xe2\x80\x8a",
88
        // LINE SEPARATOR
89
        8232 => "\xe2\x80\xa8",
90
        // PARAGRAPH SEPARATOR
91
        8233 => "\xe2\x80\xa9",
92
        // NARROW NO-BREAK SPACE
93
        8239 => "\xe2\x80\xaf",
94
        // MEDIUM MATHEMATICAL SPACE
95
        8287 => "\xe2\x81\x9f",
96
        // HALFWIDTH HANGUL FILLER
97
        65440 => "\xef\xbe\xa0",
98
        // IDEOGRAPHIC SPACE
99
        12288 => "\xe3\x80\x80",
100
    ];
101
102
    /**
103
     * @var array<string, string>
104
     */
105
    private static $WHITESPACE_TABLE = [
106
        'SPACE'                     => "\x20",
107
        'NO-BREAK SPACE'            => "\xc2\xa0",
108
        'OGHAM SPACE MARK'          => "\xe1\x9a\x80",
109
        'EN QUAD'                   => "\xe2\x80\x80",
110
        'EM QUAD'                   => "\xe2\x80\x81",
111
        'EN SPACE'                  => "\xe2\x80\x82",
112
        'EM SPACE'                  => "\xe2\x80\x83",
113
        'THREE-PER-EM SPACE'        => "\xe2\x80\x84",
114
        'FOUR-PER-EM SPACE'         => "\xe2\x80\x85",
115
        'SIX-PER-EM SPACE'          => "\xe2\x80\x86",
116
        'FIGURE SPACE'              => "\xe2\x80\x87",
117
        'PUNCTUATION SPACE'         => "\xe2\x80\x88",
118
        'THIN SPACE'                => "\xe2\x80\x89",
119
        'HAIR SPACE'                => "\xe2\x80\x8a",
120
        'LINE SEPARATOR'            => "\xe2\x80\xa8",
121
        'PARAGRAPH SEPARATOR'       => "\xe2\x80\xa9",
122
        'ZERO WIDTH SPACE'          => "\xe2\x80\x8b",
123
        'NARROW NO-BREAK SPACE'     => "\xe2\x80\xaf",
124
        'MEDIUM MATHEMATICAL SPACE' => "\xe2\x81\x9f",
125
        'IDEOGRAPHIC SPACE'         => "\xe3\x80\x80",
126
        'HALFWIDTH HANGUL FILLER'   => "\xef\xbe\xa0",
127
    ];
128
129
    /**
130
     * @var array{upper: string[], lower: string[]}
131
     */
132
    private static $COMMON_CASE_FOLD = [
133
        'upper' => [
134
            'µ',
135
            'ſ',
136
            "\xCD\x85",
137
            'ς',
138
            'ẞ',
139
            "\xCF\x90",
140
            "\xCF\x91",
141
            "\xCF\x95",
142
            "\xCF\x96",
143
            "\xCF\xB0",
144
            "\xCF\xB1",
145
            "\xCF\xB5",
146
            "\xE1\xBA\x9B",
147
            "\xE1\xBE\xBE",
148
        ],
149
        'lower' => [
150
            'μ',
151
            's',
152
            'ι',
153
            'σ',
154
            'ß',
155
            'β',
156
            'θ',
157
            'φ',
158
            'π',
159
            'κ',
160
            'ρ',
161
            'ε',
162
            "\xE1\xB9\xA1",
163
            'ι',
164
        ],
165
    ];
166
167
    /**
168
     * @var array<string, mixed>
169
     */
170
    private static $SUPPORT = [];
171
172
    /**
173
     * @var array<string, string>|null
174
     */
175
    private static $BROKEN_UTF8_FIX;
176
177
    /**
178
     * @var array<int, string>|null
179
     */
180
    private static $WIN1252_TO_UTF8;
181
182
    /**
183
     * @var array<int ,string>|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int at position 2 could not be parsed: Expected '>' at position 2, but found 'int'.
Loading history...
184
     */
185
    private static $INTL_TRANSLITERATOR_LIST;
186
187
    /**
188
     * @var array<string>|null
189
     */
190
    private static $ENCODINGS;
191
192
    /**
193
     * @var array<string ,int>|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string at position 2 could not be parsed: Expected '>' at position 2, but found 'string'.
Loading history...
194
     */
195
    private static $ORD;
196
197
    /**
198
     * @var array<string, string>|null
199
     */
200
    private static $EMOJI;
201
202
    /**
203
     * @var array<string>|null
204
     */
205
    private static $EMOJI_VALUES_CACHE;
206
207
    /**
208
     * @var array<string>|null
209
     */
210
    private static $EMOJI_KEYS_CACHE;
211
212
    /**
213
     * @var array<string>|null
214
     */
215
    private static $EMOJI_KEYS_REVERSIBLE_CACHE;
216
217
    /**
218
     * @var string[]|null
219
     *
220
     * @psalm-var array<int, string>|null
221
     */
222
    private static $CHR;
223
224
    /**
225
     * __construct()
226
     */
227 34
    public function __construct()
228
    {
229 34
    }
230
231
    /**
232
     * Return the character at the specified position: $str[1] like functionality.
233
     *
234
     * @param string $str      <p>A UTF-8 string.</p>
235
     * @param int    $pos      <p>The position of character to return.</p>
236
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
237
     *
238
     * @psalm-pure
239
     *
240
     * @return string
241
     *                <p>Single multi-byte character.</p>
242
     */
243 3
    public static function access(string $str, int $pos, string $encoding = 'UTF-8'): string
244
    {
245 3
        if ($str === '' || $pos < 0) {
246 2
            return '';
247
        }
248
249 3
        if ($encoding === 'UTF-8') {
250 3
            return (string) \mb_substr($str, $pos, 1);
251
        }
252
253
        return (string) self::substr($str, $pos, 1, $encoding);
254
    }
255
256
    /**
257
     * Prepends UTF-8 BOM character to the string and returns the whole string.
258
     *
259
     * INFO: If BOM already existed there, the Input string is returned.
260
     *
261
     * @param string $str <p>The input string.</p>
262
     *
263
     * @psalm-pure
264
     *
265
     * @return string
266
     *                <p>The output string that contains BOM.</p>
267
     */
268 2
    public static function add_bom_to_string(string $str): string
269
    {
270 2
        if (!self::string_has_bom($str)) {
271 2
            $str = self::bom() . $str;
272
        }
273
274 2
        return $str;
275
    }
276
277
    /**
278
     * Changes all keys in an array.
279
     *
280
     * @param array<string, mixed> $array    <p>The array to work on</p>
281
     * @param int                  $case     [optional] <p> Either <strong>CASE_UPPER</strong><br>
282
     *                                       or <strong>CASE_LOWER</strong> (default)</p>
283
     * @param string               $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
284
     *
285
     * @psalm-pure
286
     *
287
     * @return string[]
288
     *                  <p>An array with its keys lower- or uppercased.</p>
289
     */
290 2
    public static function array_change_key_case(
291
        array $array,
292
        int $case = \CASE_LOWER,
293
        string $encoding = 'UTF-8'
294
    ): array {
295
        if (
296 2
            $case !== \CASE_LOWER
297
            &&
298 2
            $case !== \CASE_UPPER
299
        ) {
300
            $case = \CASE_LOWER;
301
        }
302
303 2
        $return = [];
304 2
        foreach ($array as $key => &$value) {
305 2
            $key = $case === \CASE_LOWER
306 2
                ? self::strtolower((string) $key, $encoding)
307 2
                : self::strtoupper((string) $key, $encoding);
308
309 2
            $return[$key] = $value;
310
        }
311
312 2
        return $return;
313
    }
314
315
    /**
316
     * Returns the substring between $start and $end, if found, or an empty
317
     * string. An optional offset may be supplied from which to begin the
318
     * search for the start string.
319
     *
320
     * @param string $str
321
     * @param string $start    <p>Delimiter marking the start of the substring.</p>
322
     * @param string $end      <p>Delimiter marking the end of the substring.</p>
323
     * @param int    $offset   [optional] <p>Index from which to begin the search. Default: 0</p>
324
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
325
     *
326
     * @psalm-pure
327
     *
328
     * @return string
329
     */
330 16
    public static function between(
331
        string $str,
332
        string $start,
333
        string $end,
334
        int $offset = 0,
335
        string $encoding = 'UTF-8'
336
    ): string {
337 16
        if ($encoding === 'UTF-8') {
338 8
            $start_position = \mb_strpos($str, $start, $offset);
339 8
            if ($start_position === false) {
340 1
                return '';
341
            }
342
343 7
            $substr_index = $start_position + (int) \mb_strlen($start);
344 7
            $end_position = \mb_strpos($str, $end, $substr_index);
345
            if (
346 7
                $end_position === false
347
                ||
348 7
                $end_position === $substr_index
349
            ) {
350 2
                return '';
351
            }
352
353 5
            return (string) \mb_substr($str, $substr_index, $end_position - $substr_index);
354
        }
355
356 8
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
357
358 8
        $start_position = self::strpos($str, $start, $offset, $encoding);
359 8
        if ($start_position === false) {
360 1
            return '';
361
        }
362
363 7
        $substr_index = $start_position + (int) self::strlen($start, $encoding);
364 7
        $end_position = self::strpos($str, $end, $substr_index, $encoding);
365
        if (
366 7
            $end_position === false
367
            ||
368 7
            $end_position === $substr_index
369
        ) {
370 2
            return '';
371
        }
372
373 5
        return (string) self::substr(
374 5
            $str,
375 5
            $substr_index,
376 5
            $end_position - $substr_index,
377 5
            $encoding
378
        );
379
    }
380
381
    /**
382
     * Convert binary into a string.
383
     *
384
     * @param mixed $bin 1|0
385
     *
386
     * @psalm-pure
387
     *
388
     * @return string
389
     */
390 2
    public static function binary_to_str($bin): string
391
    {
392 2
        if (!isset($bin[0])) {
393
            return '';
394
        }
395
396 2
        $convert = \base_convert($bin, 2, 16);
397 2
        if ($convert === '0') {
398 1
            return '';
399
        }
400
401 2
        return \pack('H*', $convert);
402
    }
403
404
    /**
405
     * Returns the UTF-8 Byte Order Mark Character.
406
     *
407
     * INFO: take a look at UTF8::$bom for e.g. UTF-16 and UTF-32 BOM values
408
     *
409
     * @psalm-pure
410
     *
411
     * @return string
412
     *                <p>UTF-8 Byte Order Mark.</p>
413
     */
414 4
    public static function bom(): string
415
    {
416 4
        return "\xef\xbb\xbf";
417
    }
418
419
    /**
420
     * @alias of UTF8::chr_map()
421
     *
422
     * @param callable $callback
423
     * @param string   $str
424
     *
425
     * @psalm-pure
426
     *
427
     * @return string[]
428
     *
429
     * @see   UTF8::chr_map()
430
     */
431 2
    public static function callback($callback, string $str): array
432
    {
433 2
        return self::chr_map($callback, $str);
434
    }
435
436
    /**
437
     * Returns the character at $index, with indexes starting at 0.
438
     *
439
     * @param string $str      <p>The input string.</p>
440
     * @param int    $index    <p>Position of the character.</p>
441
     * @param string $encoding [optional] <p>Default is UTF-8</p>
442
     *
443
     * @psalm-pure
444
     *
445
     * @return string
446
     *                <p>The character at $index.</p>
447
     */
448 9
    public static function char_at(string $str, int $index, string $encoding = 'UTF-8'): string
449
    {
450 9
        if ($encoding === 'UTF-8') {
451 5
            return (string) \mb_substr($str, $index, 1);
452
        }
453
454 4
        return (string) self::substr($str, $index, 1, $encoding);
455
    }
456
457
    /**
458
     * Returns an array consisting of the characters in the string.
459
     *
460
     * @param string $str <p>The input string.</p>
461
     *
462
     * @psalm-pure
463
     *
464
     * @return string[]
465
     *                  <p>An array of chars.</p>
466
     */
467 3
    public static function chars(string $str): array
468
    {
469
        /** @var string[] */
470 3
        return self::str_split($str);
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::str_split($str) returns an array which contains values of type array|string[] which are incompatible with the documented value type string.
Loading history...
471
    }
472
473
    /**
474
     * This method will auto-detect your server environment for UTF-8 support.
475
     *
476
     * @return true|null
477
     *
478
     * @internal <p>You don't need to run it manually, it will be triggered if it's needed.</p>
479
     */
480 5
    public static function checkForSupport()
481
    {
482 5
        if (!isset(self::$SUPPORT['already_checked_via_portable_utf8'])) {
483
            self::$SUPPORT['already_checked_via_portable_utf8'] = true;
484
485
            // http://php.net/manual/en/book.mbstring.php
486
            self::$SUPPORT['mbstring'] = self::mbstring_loaded();
487
            self::$SUPPORT['mbstring_func_overload'] = self::mbstring_overloaded();
488
            if (self::$SUPPORT['mbstring'] === true) {
489
                \mb_internal_encoding('UTF-8');
490
                /** @noinspection UnusedFunctionResultInspection */
491
                /** @noinspection PhpComposerExtensionStubsInspection */
492
                \mb_regex_encoding('UTF-8');
493
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
494
            }
495
496
            // http://php.net/manual/en/book.iconv.php
497
            self::$SUPPORT['iconv'] = self::iconv_loaded();
498
499
            // http://php.net/manual/en/book.intl.php
500
            self::$SUPPORT['intl'] = self::intl_loaded();
501
502
            // http://php.net/manual/en/class.intlchar.php
503
            self::$SUPPORT['intlChar'] = self::intlChar_loaded();
504
505
            // http://php.net/manual/en/book.ctype.php
506
            self::$SUPPORT['ctype'] = self::ctype_loaded();
507
508
            // http://php.net/manual/en/class.finfo.php
509
            self::$SUPPORT['finfo'] = self::finfo_loaded();
510
511
            // http://php.net/manual/en/book.json.php
512
            self::$SUPPORT['json'] = self::json_loaded();
513
514
            // http://php.net/manual/en/book.pcre.php
515
            self::$SUPPORT['pcre_utf8'] = self::pcre_utf8_support();
516
517
            self::$SUPPORT['symfony_polyfill_used'] = self::symfony_polyfill_used();
518
            if (self::$SUPPORT['symfony_polyfill_used'] === true) {
519
                \mb_internal_encoding('UTF-8');
520
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
521
            }
522
523
            return true;
524
        }
525
526 5
        return null;
527
    }
528
529
    /**
530
     * Generates a UTF-8 encoded character from the given code point.
531
     *
532
     * INFO: opposite to UTF8::ord()
533
     *
534
     * @param int|string $code_point <p>The code point for which to generate a character.</p>
535
     * @param string     $encoding   [optional] <p>Default is UTF-8</p>
536
     *
537
     * @psalm-pure
538
     *
539
     * @return string|null
540
     *                     <p>Multi-byte character, returns null on failure or empty input.</p>
541
     */
542 21
    public static function chr($code_point, string $encoding = 'UTF-8')
543
    {
544
        // init
545
        /**
546
         * @psalm-suppress ImpureStaticVariable
547
         *
548
         * @var array<string,string>
549
         */
550 21
        static $CHAR_CACHE = [];
551
552 21
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
553 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
554
        }
555
556
        if (
557 21
            $encoding !== 'UTF-8'
558
            &&
559 21
            $encoding !== 'ISO-8859-1'
560
            &&
561 21
            $encoding !== 'WINDOWS-1252'
562
            &&
563 21
            self::$SUPPORT['mbstring'] === false
564
        ) {
565
            /**
566
             * @psalm-suppress ImpureFunctionCall - is is only a warning
567
             */
568
            \trigger_error('UTF8::chr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
569
        }
570
571 21
        if ($code_point <= 0) {
572 5
            return null;
573
        }
574
575 21
        $cache_key = $code_point . '_' . $encoding;
576 21
        if (isset($CHAR_CACHE[$cache_key])) {
577 19
            return $CHAR_CACHE[$cache_key];
578
        }
579
580 10
        if ($code_point <= 127) { // use "simple"-char only until "\x80"
581
582 9
            if (self::$CHR === null) {
583
                self::$CHR = self::getData('chr');
584
            }
585
586
            /**
587
             * @psalm-suppress PossiblyNullArrayAccess
588
             */
589 9
            $chr = self::$CHR[$code_point];
590
591 9
            if ($encoding !== 'UTF-8') {
592 1
                $chr = self::encode($encoding, $chr);
593
            }
594
595 9
            return $CHAR_CACHE[$cache_key] = $chr;
596
        }
597
598
        //
599
        // fallback via "IntlChar"
600
        //
601
602 6
        if (self::$SUPPORT['intlChar'] === true) {
603
            /** @noinspection PhpComposerExtensionStubsInspection */
604 6
            $chr = \IntlChar::chr($code_point);
605
606 6
            if ($encoding !== 'UTF-8') {
607
                $chr = self::encode($encoding, $chr);
608
            }
609
610 6
            return $CHAR_CACHE[$cache_key] = $chr;
611
        }
612
613
        //
614
        // fallback via vanilla php
615
        //
616
617
        if (self::$CHR === null) {
618
            self::$CHR = self::getData('chr');
619
        }
620
621
        $code_point = (int) $code_point;
622
        if ($code_point <= 0x7F) {
623
            /**
624
             * @psalm-suppress PossiblyNullArrayAccess
625
             */
626
            $chr = self::$CHR[$code_point];
627
        } elseif ($code_point <= 0x7FF) {
628
            /**
629
             * @psalm-suppress PossiblyNullArrayAccess
630
             */
631
            $chr = self::$CHR[($code_point >> 6) + 0xC0] .
632
                   self::$CHR[($code_point & 0x3F) + 0x80];
633
        } elseif ($code_point <= 0xFFFF) {
634
            /**
635
             * @psalm-suppress PossiblyNullArrayAccess
636
             */
637
            $chr = self::$CHR[($code_point >> 12) + 0xE0] .
638
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
639
                   self::$CHR[($code_point & 0x3F) + 0x80];
640
        } else {
641
            /**
642
             * @psalm-suppress PossiblyNullArrayAccess
643
             */
644
            $chr = self::$CHR[($code_point >> 18) + 0xF0] .
645
                   self::$CHR[(($code_point >> 12) & 0x3F) + 0x80] .
646
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
647
                   self::$CHR[($code_point & 0x3F) + 0x80];
648
        }
649
650
        if ($encoding !== 'UTF-8') {
651
            $chr = self::encode($encoding, $chr);
652
        }
653
654
        return $CHAR_CACHE[$cache_key] = $chr;
655
    }
656
657
    /**
658
     * Applies callback to all characters of a string.
659
     *
660
     * @param callable $callback <p>The callback function.</p>
661
     * @param string   $str      <p>UTF-8 string to run callback on.</p>
662
     *
663
     * @psalm-pure
664
     *
665
     * @return string[]
666
     *                  <p>The outcome of the callback, as array.</p>
667
     */
668 2
    public static function chr_map($callback, string $str): array
669
    {
670 2
        return \array_map(
671 2
            $callback,
672 2
            self::str_split($str)
673
        );
674
    }
675
676
    /**
677
     * Generates an array of byte length of each character of a Unicode string.
678
     *
679
     * 1 byte => U+0000  - U+007F
680
     * 2 byte => U+0080  - U+07FF
681
     * 3 byte => U+0800  - U+FFFF
682
     * 4 byte => U+10000 - U+10FFFF
683
     *
684
     * @param string $str <p>The original unicode string.</p>
685
     *
686
     * @psalm-pure
687
     *
688
     * @return int[]
689
     *               <p>An array of byte lengths of each character.</p>
690
     */
691 4
    public static function chr_size_list(string $str): array
692
    {
693 4
        if ($str === '') {
694 4
            return [];
695
        }
696
697 4
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
698
            return \array_map(
699
                static function (string $data): int {
700
                    // "mb_" is available if overload is used, so use it ...
701
                    return \mb_strlen($data, 'CP850'); // 8-BIT
702
                },
703
                self::str_split($str)
704
            );
705
        }
706
707 4
        return \array_map('\strlen', self::str_split($str));
708
    }
709
710
    /**
711
     * Get a decimal code representation of a specific character.
712
     *
713
     * @param string $char <p>The input character.</p>
714
     *
715
     * @psalm-pure
716
     *
717
     * @return int
718
     */
719 4
    public static function chr_to_decimal(string $char): int
720
    {
721 4
        if (self::$SUPPORT['iconv'] === true) {
722 4
            $chr_tmp = \iconv('UTF-8', 'UCS-4LE', $char);
723 4
            if ($chr_tmp !== false) {
724
                /** @noinspection OffsetOperationsInspection */
725 4
                return \unpack('V', $chr_tmp)[1];
726
            }
727
        }
728
729
        $code = self::ord($char[0]);
730
        $bytes = 1;
731
732
        if (!($code & 0x80)) {
733
            // 0xxxxxxx
734
            return $code;
735
        }
736
737
        if (($code & 0xe0) === 0xc0) {
738
            // 110xxxxx
739
            $bytes = 2;
740
            $code &= ~0xc0;
741
        } elseif (($code & 0xf0) === 0xe0) {
742
            // 1110xxxx
743
            $bytes = 3;
744
            $code &= ~0xe0;
745
        } elseif (($code & 0xf8) === 0xf0) {
746
            // 11110xxx
747
            $bytes = 4;
748
            $code &= ~0xf0;
749
        }
750
751
        for ($i = 2; $i <= $bytes; ++$i) {
752
            // 10xxxxxx
753
            $code = ($code << 6) + (self::ord($char[$i - 1]) & ~0x80);
754
        }
755
756
        return $code;
757
    }
758
759
    /**
760
     * Get hexadecimal code point (U+xxxx) of a UTF-8 encoded character.
761
     *
762
     * @param int|string $char   <p>The input character</p>
763
     * @param string     $prefix [optional]
764
     *
765
     * @psalm-pure
766
     *
767
     * @return string
768
     *                <p>The code point encoded as U+xxxx.</p>
769
     */
770 2
    public static function chr_to_hex($char, string $prefix = 'U+'): string
771
    {
772 2
        if ($char === '') {
773 2
            return '';
774
        }
775
776 2
        if ($char === '&#0;') {
777 2
            $char = '';
778
        }
779
780 2
        return self::int_to_hex(self::ord((string) $char), $prefix);
781
    }
782
783
    /**
784
     * alias for "UTF8::chr_to_decimal()"
785
     *
786
     * @param string $chr
787
     *
788
     * @psalm-pure
789
     *
790
     * @return int
791
     *
792
     * @see        UTF8::chr_to_decimal()
793
     * @deprecated <p>please use "UTF8::chr_to_decimal()"</p>
794
     */
795 2
    public static function chr_to_int(string $chr): int
796
    {
797 2
        return self::chr_to_decimal($chr);
798
    }
799
800
    /**
801
     * Splits a string into smaller chunks and multiple lines, using the specified line ending character.
802
     *
803
     * @param string $body         <p>The original string to be split.</p>
804
     * @param int    $chunk_length [optional] <p>The maximum character length of a chunk.</p>
805
     * @param string $end          [optional] <p>The character(s) to be inserted at the end of each chunk.</p>
806
     *
807
     * @psalm-pure
808
     *
809
     * @return string
810
     *                <p>The chunked string.</p>
811
     */
812 4
    public static function chunk_split(string $body, int $chunk_length = 76, string $end = "\r\n"): string
813
    {
814 4
        return \implode($end, self::str_split($body, $chunk_length));
815
    }
816
817
    /**
818
     * Accepts a string and removes all non-UTF-8 characters from it + extras if needed.
819
     *
820
     * @param string $str                                     <p>The string to be sanitized.</p>
821
     * @param bool   $remove_bom                              [optional] <p>Set to true, if you need to remove
822
     *                                                        UTF-BOM.</p>
823
     * @param bool   $normalize_whitespace                    [optional] <p>Set to true, if you need to normalize the
824
     *                                                        whitespace.</p>
825
     * @param bool   $normalize_msword                        [optional] <p>Set to true, if you need to normalize MS
826
     *                                                        Word chars e.g.: "…"
827
     *                                                        => "..."</p>
828
     * @param bool   $keep_non_breaking_space                 [optional] <p>Set to true, to keep non-breaking-spaces,
829
     *                                                        in
830
     *                                                        combination with
831
     *                                                        $normalize_whitespace</p>
832
     * @param bool   $replace_diamond_question_mark           [optional] <p>Set to true, if you need to remove diamond
833
     *                                                        question mark e.g.: "�"</p>
834
     * @param bool   $remove_invisible_characters             [optional] <p>Set to false, if you not want to remove
835
     *                                                        invisible characters e.g.: "\0"</p>
836
     * @param bool   $remove_invisible_characters_url_encoded [optional] <p>Set to true, if you not want to remove
837
     *                                                        invisible url encoded characters e.g.: "%0B"<br> WARNING:
838
     *                                                        maybe contains false-positives e.g. aa%0Baa -> aaaa.
839
     *                                                        </p>
840
     *
841
     * @psalm-pure
842
     *
843
     * @return string
844
     *                <p>An clean UTF-8 encoded string.</p>
845
     *
846
     * @noinspection PhpTooManyParametersInspection
847
     */
848 88
    public static function clean(
849
        string $str,
850
        bool $remove_bom = false,
851
        bool $normalize_whitespace = false,
852
        bool $normalize_msword = false,
853
        bool $keep_non_breaking_space = false,
854
        bool $replace_diamond_question_mark = false,
855
        bool $remove_invisible_characters = true,
856
        bool $remove_invisible_characters_url_encoded = false
857
    ): string {
858
        // http://stackoverflow.com/questions/1401317/remove-non-utf8-characters-from-string
859
        // caused connection reset problem on larger strings
860
861 88
        $regex = '/
862
          (
863
            (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
864
            |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
865
            |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
866
            |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3
867
            ){1,100}                      # ...one or more times
868
          )
869
        | ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
870
        | ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
871
        /x';
872
        /** @noinspection NotOptimalRegularExpressionsInspection */
873 88
        $str = (string) \preg_replace($regex, '$1', $str);
874
875 88
        if ($replace_diamond_question_mark) {
876 33
            $str = self::replace_diamond_question_mark($str, '');
877
        }
878
879 88
        if ($remove_invisible_characters) {
880 88
            $str = self::remove_invisible_characters($str, $remove_invisible_characters_url_encoded);
881
        }
882
883 88
        if ($normalize_whitespace) {
884 37
            $str = self::normalize_whitespace($str, $keep_non_breaking_space);
885
        }
886
887 88
        if ($normalize_msword) {
888 4
            $str = self::normalize_msword($str);
889
        }
890
891 88
        if ($remove_bom) {
892 37
            $str = self::remove_bom($str);
893
        }
894
895 88
        return $str;
896
    }
897
898
    /**
899
     * Clean-up a string and show only printable UTF-8 chars at the end  + fix UTF-8 encoding.
900
     *
901
     * @param string $str <p>The input string.</p>
902
     *
903
     * @psalm-pure
904
     *
905
     * @return string
906
     */
907 33
    public static function cleanup($str): string
908
    {
909
        // init
910 33
        $str = (string) $str;
911
912 33
        if ($str === '') {
913 5
            return '';
914
        }
915
916
        // fixed ISO <-> UTF-8 Errors
917 33
        $str = self::fix_simple_utf8($str);
918
919
        // remove all none UTF-8 symbols
920
        // && remove diamond question mark (�)
921
        // && remove remove invisible characters (e.g. "\0")
922
        // && remove BOM
923
        // && normalize whitespace chars (but keep non-breaking-spaces)
924 33
        return self::clean(
925 33
            $str,
926 33
            true,
927 33
            true,
928 33
            false,
929 33
            true,
930 33
            true,
931 33
            true
932
        );
933
    }
934
935
    /**
936
     * Accepts a string or a array of strings and returns an array of Unicode code points.
937
     *
938
     * INFO: opposite to UTF8::string()
939
     *
940
     * @param string|string[] $arg         <p>A UTF-8 encoded string or an array of such strings.</p>
941
     * @param bool            $use_u_style <p>If True, will return code points in U+xxxx format,
942
     *                                     default, code points will be returned as integers.</p>
943
     *
944
     * @psalm-pure
945
     *
946
     * @return array<int|string>
947
     *                           <p>
948
     *                           The array of code points:<br>
949
     *                           array<int> for $u_style === false<br>
950
     *                           array<string> for $u_style === true<br>
951
     *                           </p>
952
     */
953 12
    public static function codepoints($arg, bool $use_u_style = false): array
954
    {
955 12
        if (\is_string($arg)) {
956 12
            $arg = self::str_split($arg);
957
        }
958
959
        /**
960
         * @psalm-suppress DocblockTypeContradiction
961
         */
962 12
        if (!\is_array($arg)) {
0 ignored issues
show
introduced by
The condition is_array($arg) is always true.
Loading history...
963 4
            return [];
964
        }
965
966 12
        if ($arg === []) {
967 7
            return [];
968
        }
969
970 11
        $arg = \array_map(
971
            [
972 11
                self::class,
973
                'ord',
974
            ],
975 11
            $arg
976
        );
977
978 11
        if ($use_u_style) {
979 2
            $arg = \array_map(
980
                [
981 2
                    self::class,
982
                    'int_to_hex',
983
                ],
984 2
                $arg
985
            );
986
        }
987
988 11
        return $arg;
989
    }
990
991
    /**
992
     * Trims the string and replaces consecutive whitespace characters with a
993
     * single space. This includes tabs and newline characters, as well as
994
     * multibyte whitespace such as the thin space and ideographic space.
995
     *
996
     * @param string $str <p>The input string.</p>
997
     *
998
     * @psalm-pure
999
     *
1000
     * @return string
1001
     *                <p>A string with trimmed $str and condensed whitespace.</p>
1002
     */
1003 13
    public static function collapse_whitespace(string $str): string
1004
    {
1005 13
        if (self::$SUPPORT['mbstring'] === true) {
1006
            /** @noinspection PhpComposerExtensionStubsInspection */
1007 13
            return \trim((string) \mb_ereg_replace('[[:space:]]+', ' ', $str));
1008
        }
1009
1010
        return \trim(self::regex_replace($str, '[[:space:]]+', ' '));
1011
    }
1012
1013
    /**
1014
     * Returns count of characters used in a string.
1015
     *
1016
     * @param string $str                     <p>The input string.</p>
1017
     * @param bool   $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
1018
     * @param bool   $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
1019
     *
1020
     * @psalm-pure
1021
     *
1022
     * @return int[]
1023
     *               <p>An associative array of Character as keys and
1024
     *               their count as values.</p>
1025
     */
1026 19
    public static function count_chars(
1027
        string $str,
1028
        bool $clean_utf8 = false,
1029
        bool $try_to_use_mb_functions = true
1030
    ): array {
1031 19
        return \array_count_values(
1032 19
            self::str_split(
1033 19
                $str,
1034 19
                1,
1035 19
                $clean_utf8,
1036 19
                $try_to_use_mb_functions
1037
            )
1038
        );
1039
    }
1040
1041
    /**
1042
     * Remove css media-queries.
1043
     *
1044
     * @param string $str
1045
     *
1046
     * @psalm-pure
1047
     *
1048
     * @return string
1049
     */
1050 1
    public static function css_stripe_media_queries(string $str): string
1051
    {
1052 1
        return (string) \preg_replace(
1053 1
            '#@media\\s+(?:only\\s)?(?:[\\s{(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#isumU',
1054 1
            '',
1055 1
            $str
1056
        );
1057
    }
1058
1059
    /**
1060
     * Checks whether ctype is available on the server.
1061
     *
1062
     * @psalm-pure
1063
     *
1064
     * @return bool
1065
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
1066
     */
1067
    public static function ctype_loaded(): bool
1068
    {
1069
        return \extension_loaded('ctype');
1070
    }
1071
1072
    /**
1073
     * Converts an int value into a UTF-8 character.
1074
     *
1075
     * @param mixed $int
1076
     *
1077
     * @psalm-pure
1078
     *
1079
     * @return string
1080
     */
1081 20
    public static function decimal_to_chr($int): string
1082
    {
1083 20
        return self::html_entity_decode('&#' . $int . ';', \ENT_QUOTES | \ENT_HTML5);
1084
    }
1085
1086
    /**
1087
     * Decodes a MIME header field
1088
     *
1089
     * @param string $str
1090
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
1091
     *
1092
     * @psalm-pure
1093
     *
1094
     * @return false|string
1095
     *                      <p>A decoded MIME field on success,
1096
     *                      or false if an error occurs during the decoding.</p>
1097
     */
1098 2
    public static function decode_mimeheader($str, string $encoding = 'UTF-8')
1099
    {
1100 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1101 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1102
        }
1103
1104
        // always fallback via symfony polyfill
1105 2
        return \iconv_mime_decode($str, \ICONV_MIME_DECODE_CONTINUE_ON_ERROR, $encoding);
1106
    }
1107
1108
    /**
1109
     * Convert any two-letter country code (ISO 3166-1) to the corresponding Emoji.
1110
     *
1111
     * @see https://en.wikipedia.org/wiki/ISO_3166-1
1112
     *
1113
     * @param string $country_code_iso_3166_1 <p>e.g. DE</p>
1114
     *
1115
     * @return string
1116
     *                <p>Emoji or empty string on error.</p>
1117
     */
1118 1
    public static function emoji_from_country_code(string $country_code_iso_3166_1): string
1119
    {
1120 1
        if ($country_code_iso_3166_1 === '') {
1121 1
            return '';
1122
        }
1123
1124 1
        if (self::strlen($country_code_iso_3166_1) !== 2) {
1125 1
            return '';
1126
        }
1127
1128 1
        $country_code_iso_3166_1 = \strtoupper($country_code_iso_3166_1);
1129
1130 1
        $flagOffset = 0x1F1E6;
1131 1
        $asciiOffset = 0x41;
1132
1133 1
        return (self::chr((self::ord($country_code_iso_3166_1[0]) - $asciiOffset + $flagOffset)) ?? '') .
1134 1
               (self::chr((self::ord($country_code_iso_3166_1[1]) - $asciiOffset + $flagOffset)) ?? '');
1135
    }
1136
1137
    /**
1138
     * Decodes a string which was encoded by "UTF8::emoji_encode()".
1139
     *
1140
     * @param string $str                            <p>The input string.</p>
1141
     * @param bool   $use_reversible_string_mappings [optional] <p>
1142
     *                                               When <b>TRUE</b>, we se a reversible string mapping
1143
     *                                               between "emoji_encode" and "emoji_decode".</p>
1144
     *
1145
     * @psalm-pure
1146
     *
1147
     * @return string
1148
     */
1149 9
    public static function emoji_decode(
1150
        string $str,
1151
        bool $use_reversible_string_mappings = false
1152
    ): string {
1153 9
        self::initEmojiData();
1154
1155 9
        if ($use_reversible_string_mappings) {
1156 9
            return (string) \str_replace(
1157 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1158 9
                (array) self::$EMOJI_VALUES_CACHE,
1159 9
                $str
1160
            );
1161
        }
1162
1163 1
        return (string) \str_replace(
1164 1
            (array) self::$EMOJI_KEYS_CACHE,
1165 1
            (array) self::$EMOJI_VALUES_CACHE,
1166 1
            $str
1167
        );
1168
    }
1169
1170
    /**
1171
     * Encode a string with emoji chars into a non-emoji string.
1172
     *
1173
     * @param string $str                            <p>The input string</p>
1174
     * @param bool   $use_reversible_string_mappings [optional] <p>
1175
     *                                               when <b>TRUE</b>, we se a reversible string mapping
1176
     *                                               between "emoji_encode" and "emoji_decode"</p>
1177
     *
1178
     * @psalm-pure
1179
     *
1180
     * @return string
1181
     */
1182 12
    public static function emoji_encode(
1183
        string $str,
1184
        bool $use_reversible_string_mappings = false
1185
    ): string {
1186 12
        self::initEmojiData();
1187
1188 12
        if ($use_reversible_string_mappings) {
1189 9
            return (string) \str_replace(
1190 9
                (array) self::$EMOJI_VALUES_CACHE,
1191 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1192 9
                $str
1193
            );
1194
        }
1195
1196 4
        return (string) \str_replace(
1197 4
            (array) self::$EMOJI_VALUES_CACHE,
1198 4
            (array) self::$EMOJI_KEYS_CACHE,
1199 4
            $str
1200
        );
1201
    }
1202
1203
    /**
1204
     * Encode a string with a new charset-encoding.
1205
     *
1206
     * INFO:  This function will also try to fix broken / double encoding,
1207
     *        so you can call this function also on a UTF-8 string and you don't mess up the string.
1208
     *
1209
     * @param string $to_encoding                   <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.</p>
1210
     * @param string $str                           <p>The input string</p>
1211
     * @param bool   $auto_detect_the_from_encoding [optional] <p>Force the new encoding (we try to fix broken / double
1212
     *                                              encoding for UTF-8)<br> otherwise we auto-detect the current
1213
     *                                              string-encoding</p>
1214
     * @param string $from_encoding                 [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1215
     *                                              A empty string will trigger the autodetect anyway.</p>
1216
     *
1217
     * @psalm-pure
1218
     *
1219
     * @return string
1220
     *
1221
     * @psalm-suppress InvalidReturnStatement
1222
     */
1223 28
    public static function encode(
1224
        string $to_encoding,
1225
        string $str,
1226
        bool $auto_detect_the_from_encoding = true,
1227
        string $from_encoding = ''
1228
    ): string {
1229 28
        if ($str === '' || $to_encoding === '') {
1230 13
            return $str;
1231
        }
1232
1233 28
        if ($to_encoding !== 'UTF-8' && $to_encoding !== 'CP850') {
1234 7
            $to_encoding = self::normalize_encoding($to_encoding, 'UTF-8');
1235
        }
1236
1237 28
        if ($from_encoding && $from_encoding !== 'UTF-8' && $from_encoding !== 'CP850') {
1238 2
            $from_encoding = self::normalize_encoding($from_encoding, '');
1239
        }
1240
1241
        if (
1242 28
            $to_encoding
1243
            &&
1244 28
            $from_encoding
1245
            &&
1246 28
            $from_encoding === $to_encoding
1247
        ) {
1248
            return $str;
1249
        }
1250
1251 28
        if ($to_encoding === 'JSON') {
1252 1
            $return = self::json_encode($str);
1253 1
            if ($return === false) {
1254
                throw new \InvalidArgumentException('The input string [' . $str . '] can not be used for json_encode().');
1255
            }
1256
1257 1
            return $return;
1258
        }
1259 28
        if ($from_encoding === 'JSON') {
1260 1
            $str = self::json_decode($str);
1261 1
            $from_encoding = '';
1262
        }
1263
1264 28
        if ($to_encoding === 'BASE64') {
1265 2
            return \base64_encode($str);
1266
        }
1267 28
        if ($from_encoding === 'BASE64') {
1268 2
            $str = \base64_decode($str, true);
1269 2
            $from_encoding = '';
1270
        }
1271
1272 28
        if ($to_encoding === 'HTML-ENTITIES') {
1273 2
            return self::html_encode($str, true, 'UTF-8');
1274
        }
1275 28
        if ($from_encoding === 'HTML-ENTITIES') {
1276 2
            $str = self::html_entity_decode($str, \ENT_COMPAT, 'UTF-8');
1277 2
            $from_encoding = '';
1278
        }
1279
1280 28
        $from_encoding_auto_detected = false;
1281
        if (
1282 28
            $auto_detect_the_from_encoding
1283
            ||
1284 28
            !$from_encoding
1285
        ) {
1286 28
            $from_encoding_auto_detected = self::str_detect_encoding($str);
1287
        }
1288
1289
        // DEBUG
1290
        //var_dump($to_encoding, $from_encoding, $from_encoding_auto_detected, $str, "\n\n");
1291
1292 28
        if ($from_encoding_auto_detected !== false) {
1293
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1294 24
            $from_encoding = $from_encoding_auto_detected;
1295 7
        } elseif ($auto_detect_the_from_encoding) {
1296
            // fallback for the "autodetect"-mode
1297 7
            return self::to_utf8($str);
1298
        }
1299
1300
        if (
1301 24
            !$from_encoding
1302
            ||
1303 24
            $from_encoding === $to_encoding
1304
        ) {
1305 15
            return $str;
1306
        }
1307
1308
        if (
1309 19
            $to_encoding === 'UTF-8'
1310
            &&
1311
            (
1312 17
                $from_encoding === 'WINDOWS-1252'
1313
                ||
1314 19
                $from_encoding === 'ISO-8859-1'
1315
            )
1316
        ) {
1317 13
            return self::to_utf8($str);
1318
        }
1319
1320
        if (
1321 12
            $to_encoding === 'ISO-8859-1'
1322
            &&
1323
            (
1324 6
                $from_encoding === 'WINDOWS-1252'
1325
                ||
1326 12
                $from_encoding === 'UTF-8'
1327
            )
1328
        ) {
1329 6
            return self::to_iso8859($str);
1330
        }
1331
1332
        if (
1333 10
            $to_encoding !== 'UTF-8'
1334
            &&
1335 10
            $to_encoding !== 'ISO-8859-1'
1336
            &&
1337 10
            $to_encoding !== 'WINDOWS-1252'
1338
            &&
1339 10
            self::$SUPPORT['mbstring'] === false
1340
        ) {
1341
            /**
1342
             * @psalm-suppress ImpureFunctionCall - is is only a warning
1343
             */
1344
            \trigger_error('UTF8::encode() without mbstring cannot handle "' . $to_encoding . '" encoding', \E_USER_WARNING);
1345
        }
1346
1347 10
        if (self::$SUPPORT['mbstring'] === true) {
1348
            // warning: do not use the symfony polyfill here
1349 10
            $str_encoded = \mb_convert_encoding(
1350 10
                $str,
1351 10
                $to_encoding,
1352 10
                $from_encoding
1353
            );
1354
1355 10
            if ($str_encoded) {
1356 10
                return $str_encoded;
1357
            }
1358
        }
1359
1360
        $return = \iconv($from_encoding, $to_encoding, $str);
1361
        if ($return !== false) {
1362
            return $return;
1363
        }
1364
1365
        return $str;
1366
    }
1367
1368
    /**
1369
     * @param string $str
1370
     * @param string $from_charset      [optional] <p>Set the input charset.</p>
1371
     * @param string $to_charset        [optional] <p>Set the output charset.</p>
1372
     * @param string $transfer_encoding [optional] <p>Set the transfer encoding.</p>
1373
     * @param string $linefeed          [optional] <p>Set the used linefeed.</p>
1374
     * @param int    $indent            [optional] <p>Set the max length indent.</p>
1375
     *
1376
     * @psalm-pure
1377
     *
1378
     * @return false|string
1379
     *                      <p>An encoded MIME field on success,
1380
     *                      or false if an error occurs during the encoding.</p>
1381
     */
1382 1
    public static function encode_mimeheader(
1383
        string $str,
1384
        string $from_charset = 'UTF-8',
1385
        string $to_charset = 'UTF-8',
1386
        string $transfer_encoding = 'Q',
1387
        string $linefeed = "\r\n",
1388
        int $indent = 76
1389
    ) {
1390 1
        if ($from_charset !== 'UTF-8' && $from_charset !== 'CP850') {
1391
            $from_charset = self::normalize_encoding($from_charset, 'UTF-8');
1392
        }
1393
1394 1
        if ($to_charset !== 'UTF-8' && $to_charset !== 'CP850') {
1395 1
            $to_charset = self::normalize_encoding($to_charset, 'UTF-8');
1396
        }
1397
1398
        // always fallback via symfony polyfill
1399 1
        return \iconv_mime_encode(
1400 1
            '',
1401 1
            $str,
1402
            [
1403 1
                'scheme'           => $transfer_encoding,
1404 1
                'line-length'      => $indent,
1405 1
                'input-charset'    => $from_charset,
1406 1
                'output-charset'   => $to_charset,
1407 1
                'line-break-chars' => $linefeed,
1408
            ]
1409
        );
1410
    }
1411
1412
    /**
1413
     * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
1414
     *
1415
     * @param string   $str                       <p>The input string.</p>
1416
     * @param string   $search                    <p>The searched string.</p>
1417
     * @param int|null $length                    [optional] <p>Default: null === text->length / 2</p>
1418
     * @param string   $replacer_for_skipped_text [optional] <p>Default: …</p>
1419
     * @param string   $encoding                  [optional] <p>Set the charset for e.g. "mb_" function</p>
1420
     *
1421
     * @psalm-pure
1422
     *
1423
     * @return string
1424
     */
1425 1
    public static function extract_text(
1426
        string $str,
1427
        string $search = '',
1428
        int $length = null,
1429
        string $replacer_for_skipped_text = '…',
1430
        string $encoding = 'UTF-8'
1431
    ): string {
1432 1
        if ($str === '') {
1433 1
            return '';
1434
        }
1435
1436 1
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1437
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1438
        }
1439
1440 1
        $trim_chars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1441
1442 1
        if ($length === null) {
1443 1
            $length = (int) \round((int) self::strlen($str, $encoding) / 2, 0);
1444
        }
1445
1446 1
        if ($search === '') {
1447 1
            if ($encoding === 'UTF-8') {
1448 1
                if ($length > 0) {
1449 1
                    $string_length = (int) \mb_strlen($str);
1450 1
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1451
                } else {
1452 1
                    $end = 0;
1453
                }
1454
1455 1
                $pos = (int) \min(
1456 1
                    \mb_strpos($str, ' ', $end),
1457 1
                    \mb_strpos($str, '.', $end)
1458
                );
1459
            } else {
1460
                if ($length > 0) {
1461
                    $string_length = (int) self::strlen($str, $encoding);
1462
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1463
                } else {
1464
                    $end = 0;
1465
                }
1466
1467
                $pos = (int) \min(
1468
                    self::strpos($str, ' ', $end, $encoding),
1469
                    self::strpos($str, '.', $end, $encoding)
1470
                );
1471
            }
1472
1473 1
            if ($pos) {
1474 1
                if ($encoding === 'UTF-8') {
1475 1
                    $str_sub = \mb_substr($str, 0, $pos);
1476
                } else {
1477
                    $str_sub = self::substr($str, 0, $pos, $encoding);
1478
                }
1479
1480 1
                if ($str_sub === false) {
1481
                    return '';
1482
                }
1483
1484 1
                return \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1485
            }
1486
1487
            return $str;
1488
        }
1489
1490 1
        if ($encoding === 'UTF-8') {
1491 1
            $word_position = (int) \mb_stripos($str, $search);
1492 1
            $half_side = (int) ($word_position - $length / 2 + (int) \mb_strlen($search) / 2);
1493
        } else {
1494
            $word_position = (int) self::stripos($str, $search, 0, $encoding);
1495
            $half_side = (int) ($word_position - $length / 2 + (int) self::strlen($search, $encoding) / 2);
1496
        }
1497
1498 1
        $pos_start = 0;
1499 1
        if ($half_side > 0) {
1500 1
            if ($encoding === 'UTF-8') {
1501 1
                $half_text = \mb_substr($str, 0, $half_side);
1502
            } else {
1503
                $half_text = self::substr($str, 0, $half_side, $encoding);
1504
            }
1505 1
            if ($half_text !== false) {
1506 1
                if ($encoding === 'UTF-8') {
1507 1
                    $pos_start = (int) \max(
1508 1
                        \mb_strrpos($half_text, ' '),
1509 1
                        \mb_strrpos($half_text, '.')
1510
                    );
1511
                } else {
1512
                    $pos_start = (int) \max(
1513
                        self::strrpos($half_text, ' ', 0, $encoding),
1514
                        self::strrpos($half_text, '.', 0, $encoding)
1515
                    );
1516
                }
1517
            }
1518
        }
1519
1520 1
        if ($word_position && $half_side > 0) {
1521 1
            $offset = $pos_start + $length - 1;
1522 1
            $real_length = (int) self::strlen($str, $encoding);
1523
1524 1
            if ($offset > $real_length) {
1525
                $offset = $real_length;
1526
            }
1527
1528 1
            if ($encoding === 'UTF-8') {
1529 1
                $pos_end = (int) \min(
1530 1
                    \mb_strpos($str, ' ', $offset),
1531 1
                    \mb_strpos($str, '.', $offset)
1532 1
                ) - $pos_start;
1533
            } else {
1534
                $pos_end = (int) \min(
1535
                    self::strpos($str, ' ', $offset, $encoding),
1536
                    self::strpos($str, '.', $offset, $encoding)
1537
                ) - $pos_start;
1538
            }
1539
1540 1
            if (!$pos_end || $pos_end <= 0) {
1541 1
                if ($encoding === 'UTF-8') {
1542 1
                    $str_sub = \mb_substr($str, $pos_start, (int) \mb_strlen($str));
1543
                } else {
1544
                    $str_sub = self::substr($str, $pos_start, (int) self::strlen($str, $encoding), $encoding);
1545
                }
1546 1
                if ($str_sub !== false) {
1547 1
                    $extract = $replacer_for_skipped_text . \ltrim($str_sub, $trim_chars);
1548
                } else {
1549 1
                    $extract = '';
1550
                }
1551
            } else {
1552 1
                if ($encoding === 'UTF-8') {
1553 1
                    $str_sub = \mb_substr($str, $pos_start, $pos_end);
1554
                } else {
1555
                    $str_sub = self::substr($str, $pos_start, $pos_end, $encoding);
1556
                }
1557 1
                if ($str_sub !== false) {
1558 1
                    $extract = $replacer_for_skipped_text . \trim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1559
                } else {
1560 1
                    $extract = '';
1561
                }
1562
            }
1563
        } else {
1564 1
            $offset = $length - 1;
1565 1
            $true_length = (int) self::strlen($str, $encoding);
1566
1567 1
            if ($offset > $true_length) {
1568
                $offset = $true_length;
1569
            }
1570
1571 1
            if ($encoding === 'UTF-8') {
1572 1
                $pos_end = (int) \min(
1573 1
                    \mb_strpos($str, ' ', $offset),
1574 1
                    \mb_strpos($str, '.', $offset)
1575
                );
1576
            } else {
1577
                $pos_end = (int) \min(
1578
                    self::strpos($str, ' ', $offset, $encoding),
1579
                    self::strpos($str, '.', $offset, $encoding)
1580
                );
1581
            }
1582
1583 1
            if ($pos_end) {
1584 1
                if ($encoding === 'UTF-8') {
1585 1
                    $str_sub = \mb_substr($str, 0, $pos_end);
1586
                } else {
1587
                    $str_sub = self::substr($str, 0, $pos_end, $encoding);
1588
                }
1589 1
                if ($str_sub !== false) {
1590 1
                    $extract = \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1591
                } else {
1592 1
                    $extract = '';
1593
                }
1594
            } else {
1595 1
                $extract = $str;
1596
            }
1597
        }
1598
1599 1
        return $extract;
1600
    }
1601
1602
    /**
1603
     * Reads entire file into a string.
1604
     *
1605
     * WARNING: Do not use UTF-8 Option ($convert_to_utf8) for binary files (e.g.: images) !!!
1606
     *
1607
     * @see http://php.net/manual/en/function.file-get-contents.php
1608
     *
1609
     * @param string        $filename         <p>
1610
     *                                        Name of the file to read.
1611
     *                                        </p>
1612
     * @param bool          $use_include_path [optional] <p>
1613
     *                                        Prior to PHP 5, this parameter is called
1614
     *                                        use_include_path and is a bool.
1615
     *                                        As of PHP 5 the FILE_USE_INCLUDE_PATH can be used
1616
     *                                        to trigger include path
1617
     *                                        search.
1618
     *                                        </p>
1619
     * @param resource|null $context          [optional] <p>
1620
     *                                        A valid context resource created with
1621
     *                                        stream_context_create. If you don't need to use a
1622
     *                                        custom context, you can skip this parameter by &null;.
1623
     *                                        </p>
1624
     * @param int|null      $offset           [optional] <p>
1625
     *                                        The offset where the reading starts.
1626
     *                                        </p>
1627
     * @param int|null      $max_length       [optional] <p>
1628
     *                                        Maximum length of data read. The default is to read until end
1629
     *                                        of file is reached.
1630
     *                                        </p>
1631
     * @param int           $timeout          <p>The time in seconds for the timeout.</p>
1632
     * @param bool          $convert_to_utf8  <strong>WARNING!!!</strong> <p>Maybe you can't use this option for
1633
     *                                        some files, because they used non default utf-8 chars. Binary files
1634
     *                                        like images or pdf will not be converted.</p>
1635
     * @param string        $from_encoding    [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1636
     *                                        A empty string will trigger the autodetect anyway.</p>
1637
     *
1638
     * @psalm-pure
1639
     *
1640
     * @return false|string
1641
     *                      <p>The function returns the read data as string or <b>false</b> on failure.</p>
1642
     *
1643
     * @noinspection PhpTooManyParametersInspection
1644
     */
1645 12
    public static function file_get_contents(
1646
        string $filename,
1647
        bool $use_include_path = false,
1648
        $context = null,
1649
        int $offset = null,
1650
        int $max_length = null,
1651
        int $timeout = 10,
1652
        bool $convert_to_utf8 = true,
1653
        string $from_encoding = ''
1654
    ) {
1655
        // init
1656 12
        $filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
1657
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1658 12
        if ($filename === false) {
1659
            return false;
1660
        }
1661
1662 12
        if ($timeout && $context === null) {
1663 9
            $context = \stream_context_create(
1664
                [
1665
                    'http' => [
1666 9
                        'timeout' => $timeout,
1667
                    ],
1668
                ]
1669
            );
1670
        }
1671
1672 12
        if ($offset === null) {
1673 12
            $offset = 0;
1674
        }
1675
1676 12
        if (\is_int($max_length)) {
1677 2
            $data = \file_get_contents($filename, $use_include_path, $context, $offset, $max_length);
1678
        } else {
1679 12
            $data = \file_get_contents($filename, $use_include_path, $context, $offset);
1680
        }
1681
1682
        // return false on error
1683 12
        if ($data === false) {
1684
            return false;
1685
        }
1686
1687 12
        if ($convert_to_utf8) {
1688
            if (
1689 12
                !self::is_binary($data, true)
1690
                ||
1691 9
                self::is_utf16($data, false) !== false
1692
                ||
1693 12
                self::is_utf32($data, false) !== false
1694
            ) {
1695 9
                $data = self::encode('UTF-8', $data, false, $from_encoding);
1696 9
                $data = self::cleanup($data);
1697
            }
1698
        }
1699
1700 12
        return $data;
1701
    }
1702
1703
    /**
1704
     * Checks if a file starts with BOM (Byte Order Mark) character.
1705
     *
1706
     * @param string $file_path <p>Path to a valid file.</p>
1707
     *
1708
     * @throws \RuntimeException if file_get_contents() returned false
1709
     *
1710
     * @return bool
1711
     *              <p><strong>true</strong> if the file has BOM at the start, <strong>false</strong> otherwise</p>
1712
     *
1713
     * @psalm-pure
1714
     */
1715 2
    public static function file_has_bom(string $file_path): bool
1716
    {
1717 2
        $file_content = \file_get_contents($file_path);
1718 2
        if ($file_content === false) {
1719
            throw new \RuntimeException('file_get_contents() returned false for:' . $file_path);
1720
        }
1721
1722 2
        return self::string_has_bom($file_content);
1723
    }
1724
1725
    /**
1726
     * Normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1727
     *
1728
     * @param mixed  $var
1729
     * @param int    $normalization_form
1730
     * @param string $leading_combining
1731
     *
1732
     * @psalm-pure
1733
     *
1734
     * @return mixed
1735
     */
1736 64
    public static function filter(
1737
        $var,
1738
        int $normalization_form = \Normalizer::NFC,
1739
        string $leading_combining = '◌'
1740
    ) {
1741 64
        switch (\gettype($var)) {
1742 64
            case 'object':
1743 64
            case 'array':
1744 6
                foreach ($var as $k => &$v) {
1745 6
                    $v = self::filter($v, $normalization_form, $leading_combining);
1746
                }
1747 6
                unset($v);
1748
1749 6
                break;
1750 64
            case 'string':
1751
1752 62
                if (\strpos($var, "\r") !== false) {
1753
                    // Workaround https://bugs.php.net/65732
1754 3
                    $var = self::normalize_line_ending($var);
1755
                }
1756
1757 62
                if (!ASCII::is_ascii($var)) {
1758 32
                    if (\Normalizer::isNormalized($var, $normalization_form)) {
1759 27
                        $n = '-';
1760
                    } else {
1761 12
                        $n = \Normalizer::normalize($var, $normalization_form);
1762
1763 12
                        if (isset($n[0])) {
1764 7
                            $var = $n;
1765
                        } else {
1766 8
                            $var = self::encode('UTF-8', $var, true);
1767
                        }
1768
                    }
1769
1770
                    if (
1771 32
                        $var[0] >= "\x80"
1772
                        &&
1773 32
                        isset($n[0], $leading_combining[0])
1774
                        &&
1775 32
                        \preg_match('/^\\p{Mn}/u', $var)
1776
                    ) {
1777
                        // Prevent leading combining chars
1778
                        // for NFC-safe concatenations.
1779 3
                        $var = $leading_combining . $var;
1780
                    }
1781
                }
1782
1783 62
                break;
1784
        }
1785
1786 64
        return $var;
1787
    }
1788
1789
    /**
1790
     * "filter_input()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1791
     *
1792
     * Gets a specific external variable by name and optionally filters it
1793
     *
1794
     * @see http://php.net/manual/en/function.filter-input.php
1795
     *
1796
     * @param int    $type          <p>
1797
     *                              One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1798
     *                              <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1799
     *                              <b>INPUT_ENV</b>.
1800
     *                              </p>
1801
     * @param string $variable_name <p>
1802
     *                              Name of a variable to get.
1803
     *                              </p>
1804
     * @param int    $filter        [optional] <p>
1805
     *                              The ID of the filter to apply. The
1806
     *                              manual page lists the available filters.
1807
     *                              </p>
1808
     * @param mixed  $options       [optional] <p>
1809
     *                              Associative array of options or bitwise disjunction of flags. If filter
1810
     *                              accepts options, flags can be provided in "flags" field of array.
1811
     *                              </p>
1812
     *
1813
     * @psalm-pure
1814
     *
1815
     * @return mixed
1816
     *               <p>
1817
     *               Value of the requested variable on success, <b>FALSE</b> if the filter fails, or <b>NULL</b> if the
1818
     *               <i>variable_name</i> variable is not set. If the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it
1819
     *               returns <b>FALSE</b> if the variable is not set and <b>NULL</b> if the filter fails.
1820
     *               </p>
1821
     */
1822 1
    public static function filter_input(
1823
        int $type,
1824
        string $variable_name,
1825
        int $filter = \FILTER_DEFAULT,
1826
        $options = null
1827
    ) {
1828
        /**
1829
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1830
         */
1831 1
        if ($options === null || \func_num_args() < 4) {
1832 1
            $var = \filter_input($type, $variable_name, $filter);
1833
        } else {
1834
            $var = \filter_input($type, $variable_name, $filter, $options);
1835
        }
1836
1837 1
        return self::filter($var);
1838
    }
1839
1840
    /**
1841
     * "filter_input_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1842
     *
1843
     * Gets external variables and optionally filters them
1844
     *
1845
     * @see http://php.net/manual/en/function.filter-input-array.php
1846
     *
1847
     * @param int   $type       <p>
1848
     *                          One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1849
     *                          <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1850
     *                          <b>INPUT_ENV</b>.
1851
     *                          </p>
1852
     * @param mixed $definition [optional] <p>
1853
     *                          An array defining the arguments. A valid key is a string
1854
     *                          containing a variable name and a valid value is either a filter type, or an array
1855
     *                          optionally specifying the filter, flags and options. If the value is an
1856
     *                          array, valid keys are filter which specifies the
1857
     *                          filter type,
1858
     *                          flags which specifies any flags that apply to the
1859
     *                          filter, and options which specifies any options that
1860
     *                          apply to the filter. See the example below for a better understanding.
1861
     *                          </p>
1862
     *                          <p>
1863
     *                          This parameter can be also an integer holding a filter constant. Then all values in the
1864
     *                          input array are filtered by this filter.
1865
     *                          </p>
1866
     * @param bool  $add_empty  [optional] <p>
1867
     *                          Add missing keys as <b>NULL</b> to the return value.
1868
     *                          </p>
1869
     *
1870
     * @psalm-pure
1871
     *
1872
     * @return mixed
1873
     *               <p>
1874
     *               An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1875
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1876
     *               set. Or if the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it returns <b>FALSE</b> if the variable
1877
     *               is not set and <b>NULL</b> if the filter fails.
1878
     *               </p>
1879
     */
1880 1
    public static function filter_input_array(
1881
        int $type,
1882
        $definition = null,
1883
        bool $add_empty = true
1884
    ) {
1885
        /**
1886
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1887
         */
1888 1
        if ($definition === null || \func_num_args() < 2) {
1889
            $a = \filter_input_array($type);
1890
        } else {
1891 1
            $a = \filter_input_array($type, $definition, $add_empty);
1892
        }
1893
1894 1
        return self::filter($a);
1895
    }
1896
1897
    /**
1898
     * "filter_var()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1899
     *
1900
     * Filters a variable with a specified filter
1901
     *
1902
     * @see http://php.net/manual/en/function.filter-var.php
1903
     *
1904
     * @param mixed $variable <p>
1905
     *                        Value to filter.
1906
     *                        </p>
1907
     * @param int   $filter   [optional] <p>
1908
     *                        The ID of the filter to apply. The
1909
     *                        manual page lists the available filters.
1910
     *                        </p>
1911
     * @param mixed $options  [optional] <p>
1912
     *                        Associative array of options or bitwise disjunction of flags. If filter
1913
     *                        accepts options, flags can be provided in "flags" field of array. For
1914
     *                        the "callback" filter, callable type should be passed. The
1915
     *                        callback must accept one argument, the value to be filtered, and return
1916
     *                        the value after filtering/sanitizing it.
1917
     *                        </p>
1918
     *                        <p>
1919
     *                        <code>
1920
     *                        // for filters that accept options, use this format
1921
     *                        $options = array(
1922
     *                        'options' => array(
1923
     *                        'default' => 3, // value to return if the filter fails
1924
     *                        // other options here
1925
     *                        'min_range' => 0
1926
     *                        ),
1927
     *                        'flags' => FILTER_FLAG_ALLOW_OCTAL,
1928
     *                        );
1929
     *                        $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
1930
     *                        // for filter that only accept flags, you can pass them directly
1931
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
1932
     *                        // for filter that only accept flags, you can also pass as an array
1933
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN,
1934
     *                        array('flags' => FILTER_NULL_ON_FAILURE));
1935
     *                        // callback validate filter
1936
     *                        function foo($value)
1937
     *                        {
1938
     *                        // Expected format: Surname, GivenNames
1939
     *                        if (strpos($value, ", ") === false) return false;
1940
     *                        list($surname, $givennames) = explode(", ", $value, 2);
1941
     *                        $empty = (empty($surname) || empty($givennames));
1942
     *                        $notstrings = (!is_string($surname) || !is_string($givennames));
1943
     *                        if ($empty || $notstrings) {
1944
     *                        return false;
1945
     *                        } else {
1946
     *                        return $value;
1947
     *                        }
1948
     *                        }
1949
     *                        $var = filter_var('Doe, Jane Sue', FILTER_CALLBACK, array('options' => 'foo'));
1950
     *                        </code>
1951
     *                        </p>
1952
     *
1953
     * @psalm-pure
1954
     *
1955
     * @return mixed
1956
     *               <p>The filtered data, or <b>FALSE</b> if the filter fails.</p>
1957
     */
1958 2
    public static function filter_var(
1959
        $variable,
1960
        int $filter = \FILTER_DEFAULT,
1961
        $options = null
1962
    ) {
1963
        /**
1964
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1965
         */
1966 2
        if (\func_num_args() < 3) {
1967 2
            $variable = \filter_var($variable, $filter);
1968
        } else {
1969 2
            $variable = \filter_var($variable, $filter, $options);
1970
        }
1971
1972 2
        return self::filter($variable);
1973
    }
1974
1975
    /**
1976
     * "filter_var_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1977
     *
1978
     * Gets multiple variables and optionally filters them
1979
     *
1980
     * @see http://php.net/manual/en/function.filter-var-array.php
1981
     *
1982
     * @param array<mixed> $data       <p>
1983
     *                                 An array with string keys containing the data to filter.
1984
     *                                 </p>
1985
     * @param mixed        $definition [optional] <p>
1986
     *                                 An array defining the arguments. A valid key is a string
1987
     *                                 containing a variable name and a valid value is either a
1988
     *                                 filter type, or an
1989
     *                                 array optionally specifying the filter, flags and options.
1990
     *                                 If the value is an array, valid keys are filter
1991
     *                                 which specifies the filter type,
1992
     *                                 flags which specifies any flags that apply to the
1993
     *                                 filter, and options which specifies any options that
1994
     *                                 apply to the filter. See the example below for a better understanding.
1995
     *                                 </p>
1996
     *                                 <p>
1997
     *                                 This parameter can be also an integer holding a filter constant. Then all values
1998
     *                                 in the input array are filtered by this filter.
1999
     *                                 </p>
2000
     * @param bool         $add_empty  [optional] <p>
2001
     *                                 Add missing keys as <b>NULL</b> to the return value.
2002
     *                                 </p>
2003
     *
2004
     * @psalm-pure
2005
     *
2006
     * @return mixed
2007
     *               <p>
2008
     *               An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
2009
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
2010
     *               set.
2011
     *               </p>
2012
     */
2013 2
    public static function filter_var_array(
2014
        array $data,
2015
        $definition = null,
2016
        bool $add_empty = true
2017
    ) {
2018
        /**
2019
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
2020
         */
2021 2
        if (\func_num_args() < 2) {
2022 2
            $a = \filter_var_array($data);
2023
        } else {
2024 2
            $a = \filter_var_array($data, $definition, $add_empty);
2025
        }
2026
2027 2
        return self::filter($a);
2028
    }
2029
2030
    /**
2031
     * Checks whether finfo is available on the server.
2032
     *
2033
     * @psalm-pure
2034
     *
2035
     * @return bool
2036
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
2037
     */
2038
    public static function finfo_loaded(): bool
2039
    {
2040
        return \class_exists('finfo');
2041
    }
2042
2043
    /**
2044
     * Returns the first $n characters of the string.
2045
     *
2046
     * @param string $str      <p>The input string.</p>
2047
     * @param int    $n        <p>Number of characters to retrieve from the start.</p>
2048
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2049
     *
2050
     * @psalm-pure
2051
     *
2052
     * @return string
2053
     */
2054 13
    public static function first_char(
2055
        string $str,
2056
        int $n = 1,
2057
        string $encoding = 'UTF-8'
2058
    ): string {
2059 13
        if ($str === '' || $n <= 0) {
2060 5
            return '';
2061
        }
2062
2063 8
        if ($encoding === 'UTF-8') {
2064 4
            return (string) \mb_substr($str, 0, $n);
2065
        }
2066
2067 4
        return (string) self::substr($str, 0, $n, $encoding);
2068
    }
2069
2070
    /**
2071
     * Check if the number of Unicode characters isn't greater than the specified integer.
2072
     *
2073
     * @param string $str      the original string to be checked
2074
     * @param int    $box_size the size in number of chars to be checked against string
2075
     *
2076
     * @psalm-pure
2077
     *
2078
     * @return bool
2079
     *              <p><strong>TRUE</strong> if string is less than or equal to $box_size, <strong>FALSE</strong> otherwise.</p>
2080
     */
2081 2
    public static function fits_inside(string $str, int $box_size): bool
2082
    {
2083 2
        return (int) self::strlen($str) <= $box_size;
2084
    }
2085
2086
    /**
2087
     * Try to fix simple broken UTF-8 strings.
2088
     *
2089
     * INFO: Take a look at "UTF8::fix_utf8()" if you need a more advanced fix for broken UTF-8 strings.
2090
     *
2091
     * If you received an UTF-8 string that was converted from Windows-1252 as it was ISO-8859-1
2092
     * (ignoring Windows-1252 chars from 80 to 9F) use this function to fix it.
2093
     * See: http://en.wikipedia.org/wiki/Windows-1252
2094
     *
2095
     * @param string $str <p>The input string</p>
2096
     *
2097
     * @psalm-pure
2098
     *
2099
     * @return string
2100
     */
2101 47
    public static function fix_simple_utf8(string $str): string
2102
    {
2103 47
        if ($str === '') {
2104 4
            return '';
2105
        }
2106
2107
        /**
2108
         * @psalm-suppress ImpureStaticVariable
2109
         *
2110
         * @var array<mixed>|null
2111
         */
2112 47
        static $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = null;
2113
2114
        /**
2115
         * @psalm-suppress ImpureStaticVariable
2116
         *
2117
         * @var array<mixed>|null
2118
         */
2119 47
        static $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = null;
2120
2121 47
        if ($BROKEN_UTF8_TO_UTF8_KEYS_CACHE === null) {
2122 1
            if (self::$BROKEN_UTF8_FIX === null) {
2123 1
                self::$BROKEN_UTF8_FIX = self::getData('utf8_fix');
2124
            }
2125
2126 1
            $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = \array_keys(self::$BROKEN_UTF8_FIX);
2127 1
            $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = self::$BROKEN_UTF8_FIX;
2128
        }
2129
2130 47
        \assert(\is_array($BROKEN_UTF8_TO_UTF8_VALUES_CACHE));
2131
2132 47
        return \str_replace($BROKEN_UTF8_TO_UTF8_KEYS_CACHE, $BROKEN_UTF8_TO_UTF8_VALUES_CACHE, $str);
2133
    }
2134
2135
    /**
2136
     * Fix a double (or multiple) encoded UTF8 string.
2137
     *
2138
     * @param string|string[] $str you can use a string or an array of strings
2139
     *
2140
     * @psalm-pure
2141
     *
2142
     * @return string|string[]
2143
     *                         Will return the fixed input-"array" or
2144
     *                         the fixed input-"string"
2145
     *
2146
     * @psalm-suppress InvalidReturnType
2147
     */
2148 2
    public static function fix_utf8($str)
2149
    {
2150 2
        if (\is_array($str)) {
2151 2
            foreach ($str as $k => &$v) {
2152 2
                $v = self::fix_utf8($v);
2153
            }
2154 2
            unset($v);
2155
2156
            /**
2157
             * @psalm-suppress InvalidReturnStatement
2158
             */
2159 2
            return $str;
2160
        }
2161
2162 2
        $str = (string) $str;
2163 2
        $last = '';
2164 2
        while ($last !== $str) {
2165 2
            $last = $str;
2166
            /**
2167
             * @psalm-suppress PossiblyInvalidArgument
2168
             */
2169 2
            $str = self::to_utf8(
2170 2
                self::utf8_decode($str, true)
2171
            );
2172
        }
2173
2174
        /**
2175
         * @psalm-suppress InvalidReturnStatement
2176
         */
2177 2
        return $str;
2178
    }
2179
2180
    /**
2181
     * Get character of a specific character.
2182
     *
2183
     * @param string $char
2184
     *
2185
     * @psalm-pure
2186
     *
2187
     * @return string
2188
     *                <p>'RTL' or 'LTR'.</p>
2189
     */
2190 2
    public static function getCharDirection(string $char): string
2191
    {
2192 2
        if (self::$SUPPORT['intlChar'] === true) {
2193
            /** @noinspection PhpComposerExtensionStubsInspection */
2194 2
            $tmp_return = \IntlChar::charDirection($char);
2195
2196
            // from "IntlChar"-Class
2197
            $char_direction = [
2198 2
                'RTL' => [1, 13, 14, 15, 21],
2199
                'LTR' => [0, 11, 12, 20],
2200
            ];
2201
2202 2
            if (\in_array($tmp_return, $char_direction['LTR'], true)) {
2203
                return 'LTR';
2204
            }
2205
2206 2
            if (\in_array($tmp_return, $char_direction['RTL'], true)) {
2207 2
                return 'RTL';
2208
            }
2209
        }
2210
2211 2
        $c = static::chr_to_decimal($char);
2212
2213 2
        if (!($c >= 0x5be && $c <= 0x10b7f)) {
2214 2
            return 'LTR';
2215
        }
2216
2217 2
        if ($c <= 0x85e) {
2218 2
            if ($c === 0x5be ||
2219 2
                $c === 0x5c0 ||
2220 2
                $c === 0x5c3 ||
2221 2
                $c === 0x5c6 ||
2222 2
                ($c >= 0x5d0 && $c <= 0x5ea) ||
2223 2
                ($c >= 0x5f0 && $c <= 0x5f4) ||
2224 2
                $c === 0x608 ||
2225 2
                $c === 0x60b ||
2226 2
                $c === 0x60d ||
2227 2
                $c === 0x61b ||
2228 2
                ($c >= 0x61e && $c <= 0x64a) ||
2229
                ($c >= 0x66d && $c <= 0x66f) ||
2230
                ($c >= 0x671 && $c <= 0x6d5) ||
2231
                ($c >= 0x6e5 && $c <= 0x6e6) ||
2232
                ($c >= 0x6ee && $c <= 0x6ef) ||
2233
                ($c >= 0x6fa && $c <= 0x70d) ||
2234
                $c === 0x710 ||
2235
                ($c >= 0x712 && $c <= 0x72f) ||
2236
                ($c >= 0x74d && $c <= 0x7a5) ||
2237
                $c === 0x7b1 ||
2238
                ($c >= 0x7c0 && $c <= 0x7ea) ||
2239
                ($c >= 0x7f4 && $c <= 0x7f5) ||
2240
                $c === 0x7fa ||
2241
                ($c >= 0x800 && $c <= 0x815) ||
2242
                $c === 0x81a ||
2243
                $c === 0x824 ||
2244
                $c === 0x828 ||
2245
                ($c >= 0x830 && $c <= 0x83e) ||
2246
                ($c >= 0x840 && $c <= 0x858) ||
2247 2
                $c === 0x85e
2248
            ) {
2249 2
                return 'RTL';
2250
            }
2251 2
        } elseif ($c === 0x200f) {
2252
            return 'RTL';
2253 2
        } elseif ($c >= 0xfb1d) {
2254 2
            if ($c === 0xfb1d ||
2255 2
                ($c >= 0xfb1f && $c <= 0xfb28) ||
2256 2
                ($c >= 0xfb2a && $c <= 0xfb36) ||
2257 2
                ($c >= 0xfb38 && $c <= 0xfb3c) ||
2258 2
                $c === 0xfb3e ||
2259 2
                ($c >= 0xfb40 && $c <= 0xfb41) ||
2260 2
                ($c >= 0xfb43 && $c <= 0xfb44) ||
2261 2
                ($c >= 0xfb46 && $c <= 0xfbc1) ||
2262 2
                ($c >= 0xfbd3 && $c <= 0xfd3d) ||
2263 2
                ($c >= 0xfd50 && $c <= 0xfd8f) ||
2264 2
                ($c >= 0xfd92 && $c <= 0xfdc7) ||
2265 2
                ($c >= 0xfdf0 && $c <= 0xfdfc) ||
2266 2
                ($c >= 0xfe70 && $c <= 0xfe74) ||
2267 2
                ($c >= 0xfe76 && $c <= 0xfefc) ||
2268 2
                ($c >= 0x10800 && $c <= 0x10805) ||
2269 2
                $c === 0x10808 ||
2270 2
                ($c >= 0x1080a && $c <= 0x10835) ||
2271 2
                ($c >= 0x10837 && $c <= 0x10838) ||
2272 2
                $c === 0x1083c ||
2273 2
                ($c >= 0x1083f && $c <= 0x10855) ||
2274 2
                ($c >= 0x10857 && $c <= 0x1085f) ||
2275 2
                ($c >= 0x10900 && $c <= 0x1091b) ||
2276 2
                ($c >= 0x10920 && $c <= 0x10939) ||
2277 2
                $c === 0x1093f ||
2278 2
                $c === 0x10a00 ||
2279 2
                ($c >= 0x10a10 && $c <= 0x10a13) ||
2280 2
                ($c >= 0x10a15 && $c <= 0x10a17) ||
2281 2
                ($c >= 0x10a19 && $c <= 0x10a33) ||
2282 2
                ($c >= 0x10a40 && $c <= 0x10a47) ||
2283 2
                ($c >= 0x10a50 && $c <= 0x10a58) ||
2284 2
                ($c >= 0x10a60 && $c <= 0x10a7f) ||
2285 2
                ($c >= 0x10b00 && $c <= 0x10b35) ||
2286 2
                ($c >= 0x10b40 && $c <= 0x10b55) ||
2287 2
                ($c >= 0x10b58 && $c <= 0x10b72) ||
2288 2
                ($c >= 0x10b78 && $c <= 0x10b7f)
2289
            ) {
2290 2
                return 'RTL';
2291
            }
2292
        }
2293
2294 2
        return 'LTR';
2295
    }
2296
2297
    /**
2298
     * Check for php-support.
2299
     *
2300
     * @param string|null $key
2301
     *
2302
     * @psalm-pure
2303
     *
2304
     * @return mixed
2305
     *               Return the full support-"array", if $key === null<br>
2306
     *               return bool-value, if $key is used and available<br>
2307
     *               otherwise return <strong>null</strong>
2308
     */
2309 27
    public static function getSupportInfo(string $key = null)
2310
    {
2311 27
        if ($key === null) {
2312 4
            return self::$SUPPORT;
2313
        }
2314
2315 25
        if (self::$INTL_TRANSLITERATOR_LIST === null) {
2316 1
            self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
2317
        }
2318
        // compatibility fix for old versions
2319 25
        self::$SUPPORT['intl__transliterator_list_ids'] = self::$INTL_TRANSLITERATOR_LIST;
2320
2321 25
        return self::$SUPPORT[$key] ?? null;
2322
    }
2323
2324
    /**
2325
     * Warning: this method only works for some file-types (png, jpg)
2326
     *          if you need more supported types, please use e.g. "finfo"
2327
     *
2328
     * @param string $str
2329
     * @param array  $fallback <p>with this keys: 'ext', 'mime', 'type'
2330
     *
2331
     * @psalm-pure
2332
     *
2333
     * @return array<string, string|null>
2334
     *                       <p>with this keys: 'ext', 'mime', 'type'</p>
2335
     *
2336
     * @phpstan-param array{ext: null|string, mime: null|string, type: null|string} $fallback
2337
     */
2338 39
    public static function get_file_type(
2339
        string $str,
2340
        array $fallback = [
2341
            'ext'  => null,
2342
            'mime' => 'application/octet-stream',
2343
            'type' => null,
2344
        ]
2345
    ): array {
2346 39
        if ($str === '') {
2347
            return $fallback;
2348
        }
2349
2350
        /** @var false|string $str_info - needed for PhpStan (stubs error) */
2351 39
        $str_info = \substr($str, 0, 2);
2352 39
        if ($str_info === false || \strlen($str_info) !== 2) {
2353 11
            return $fallback;
2354
        }
2355
2356
        // DEBUG
2357
        //var_dump($str_info);
2358
2359 35
        $str_info = \unpack('C2chars', $str_info);
2360
2361
        /** @noinspection PhpSillyAssignmentInspection */
2362
        /** @var array|false $str_info - needed for PhpStan (stubs error) */
2363 35
        $str_info = $str_info;
2364
2365 35
        if ($str_info === false) {
2366
            return $fallback;
2367
        }
2368
        /** @noinspection OffsetOperationsInspection */
2369 35
        $type_code = (int) ($str_info['chars1'] . $str_info['chars2']);
2370
2371
        // DEBUG
2372
        //var_dump($type_code);
2373
2374
        //
2375
        // info: https://en.wikipedia.org/wiki/Magic_number_%28programming%29#Format_indicator
2376
        //
2377
        switch ($type_code) {
2378
            // WARNING: do not add too simple comparisons, because of false-positive results:
2379
            //
2380
            // 3780 => 'pdf', 7790 => 'exe', 7784 => 'midi', 8075 => 'zip',
2381
            // 8297 => 'rar', 7173 => 'gif', 7373 => 'tiff' 6677 => 'bmp', ...
2382
            //
2383 35
            case 255216:
2384
                $ext = 'jpg';
2385
                $mime = 'image/jpeg';
2386
                $type = 'binary';
2387
2388
                break;
2389 35
            case 13780:
2390 7
                $ext = 'png';
2391 7
                $mime = 'image/png';
2392 7
                $type = 'binary';
2393
2394 7
                break;
2395
            default:
2396 34
                return $fallback;
2397
        }
2398
2399
        return [
2400 7
            'ext'  => $ext,
2401 7
            'mime' => $mime,
2402 7
            'type' => $type,
2403
        ];
2404
    }
2405
2406
    /**
2407
     * @param int    $length         <p>Length of the random string.</p>
2408
     * @param string $possible_chars [optional] <p>Characters string for the random selection.</p>
2409
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
2410
     *
2411
     * @return string
2412
     */
2413 1
    public static function get_random_string(
2414
        int $length,
2415
        string $possible_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
2416
        string $encoding = 'UTF-8'
2417
    ): string {
2418
        // init
2419 1
        $i = 0;
2420 1
        $str = '';
2421
2422
        //
2423
        // add random chars
2424
        //
2425
2426 1
        if ($encoding === 'UTF-8') {
2427 1
            $max_length = (int) \mb_strlen($possible_chars);
2428 1
            if ($max_length === 0) {
2429 1
                return '';
2430
            }
2431
2432 1
            while ($i < $length) {
2433
                try {
2434 1
                    $rand_int = \random_int(0, $max_length - 1);
2435
                } catch (\Exception $e) {
2436
                    /** @noinspection RandomApiMigrationInspection */
2437
                    $rand_int = \mt_rand(0, $max_length - 1);
2438
                }
2439 1
                $char = \mb_substr($possible_chars, $rand_int, 1);
2440 1
                if ($char !== false) {
2441 1
                    $str .= $char;
2442 1
                    ++$i;
2443
                }
2444
            }
2445
        } else {
2446
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2447
2448
            $max_length = (int) self::strlen($possible_chars, $encoding);
2449
            if ($max_length === 0) {
2450
                return '';
2451
            }
2452
2453
            while ($i < $length) {
2454
                try {
2455
                    $rand_int = \random_int(0, $max_length - 1);
2456
                } catch (\Exception $e) {
2457
                    /** @noinspection RandomApiMigrationInspection */
2458
                    $rand_int = \mt_rand(0, $max_length - 1);
2459
                }
2460
                $char = self::substr($possible_chars, $rand_int, 1, $encoding);
2461
                if ($char !== false) {
2462
                    $str .= $char;
2463
                    ++$i;
2464
                }
2465
            }
2466
        }
2467
2468 1
        return $str;
2469
    }
2470
2471
    /**
2472
     * @param int|string $extra_entropy [optional] <p>Extra entropy via a string or int value.</p>
2473
     * @param bool       $use_md5       [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
2474
     *
2475
     * @return string
2476
     */
2477 1
    public static function get_unique_string($extra_entropy = '', bool $use_md5 = true): string
2478
    {
2479
        try {
2480 1
            $rand_int = \random_int(0, \mt_getrandmax());
2481
        } catch (\Exception $e) {
2482
            /** @noinspection RandomApiMigrationInspection */
2483
            $rand_int = \mt_rand(0, \mt_getrandmax());
2484
        }
2485
2486
        $unique_helper = $rand_int .
2487 1
                         \session_id() .
2488 1
                         ($_SERVER['REMOTE_ADDR'] ?? '') .
2489 1
                         ($_SERVER['SERVER_ADDR'] ?? '') .
2490 1
                         $extra_entropy;
2491
2492 1
        $unique_string = \uniqid($unique_helper, true);
2493
2494 1
        if ($use_md5) {
2495 1
            $unique_string = \md5($unique_string . $unique_helper);
2496
        }
2497
2498 1
        return $unique_string;
2499
    }
2500
2501
    /**
2502
     * alias for "UTF8::string_has_bom()"
2503
     *
2504
     * @param string $str
2505
     *
2506
     * @psalm-pure
2507
     *
2508
     * @return bool
2509
     *
2510
     * @see        UTF8::string_has_bom()
2511
     * @deprecated <p>please use "UTF8::string_has_bom()"</p>
2512
     */
2513 2
    public static function hasBom(string $str): bool
2514
    {
2515 2
        return self::string_has_bom($str);
2516
    }
2517
2518
    /**
2519
     * Returns true if the string contains a lower case char, false otherwise.
2520
     *
2521
     * @param string $str <p>The input string.</p>
2522
     *
2523
     * @psalm-pure
2524
     *
2525
     * @return bool
2526
     *              <p>Whether or not the string contains a lower case character.</p>
2527
     */
2528 47
    public static function has_lowercase(string $str): bool
2529
    {
2530 47
        if (self::$SUPPORT['mbstring'] === true) {
2531
            /** @noinspection PhpComposerExtensionStubsInspection */
2532 47
            return \mb_ereg_match('.*[[:lower:]]', $str);
2533
        }
2534
2535
        return self::str_matches_pattern($str, '.*[[:lower:]]');
2536
    }
2537
2538
    /**
2539
     * Returns true if the string contains whitespace, false otherwise.
2540
     *
2541
     * @param string $str <p>The input string.</p>
2542
     *
2543
     * @psalm-pure
2544
     *
2545
     * @return bool
2546
     *              <p>Whether or not the string contains whitespace.</p>
2547
     */
2548 11
    public static function has_whitespace(string $str): bool
2549
    {
2550 11
        if (self::$SUPPORT['mbstring'] === true) {
2551
            /** @noinspection PhpComposerExtensionStubsInspection */
2552 11
            return \mb_ereg_match('.*[[:space:]]', $str);
2553
        }
2554
2555
        return self::str_matches_pattern($str, '.*[[:space:]]');
2556
    }
2557
2558
    /**
2559
     * Returns true if the string contains an upper case char, false otherwise.
2560
     *
2561
     * @param string $str <p>The input string.</p>
2562
     *
2563
     * @psalm-pure
2564
     *
2565
     * @return bool whether or not the string contains an upper case character
2566
     */
2567 12
    public static function has_uppercase(string $str): bool
2568
    {
2569 12
        if (self::$SUPPORT['mbstring'] === true) {
2570
            /** @noinspection PhpComposerExtensionStubsInspection */
2571 12
            return \mb_ereg_match('.*[[:upper:]]', $str);
2572
        }
2573
2574
        return self::str_matches_pattern($str, '.*[[:upper:]]');
2575
    }
2576
2577
    /**
2578
     * Converts a hexadecimal value into a UTF-8 character.
2579
     *
2580
     * @param string $hexdec <p>The hexadecimal value.</p>
2581
     *
2582
     * @psalm-pure
2583
     *
2584
     * @return false|string one single UTF-8 character
2585
     */
2586 4
    public static function hex_to_chr(string $hexdec)
2587
    {
2588 4
        return self::decimal_to_chr(\hexdec($hexdec));
2589
    }
2590
2591
    /**
2592
     * Converts hexadecimal U+xxxx code point representation to integer.
2593
     *
2594
     * INFO: opposite to UTF8::int_to_hex()
2595
     *
2596
     * @param string $hexdec <p>The hexadecimal code point representation.</p>
2597
     *
2598
     * @psalm-pure
2599
     *
2600
     * @return false|int
2601
     *                   <p>The code point, or false on failure.</p>
2602
     */
2603 2
    public static function hex_to_int($hexdec)
2604
    {
2605
        // init
2606 2
        $hexdec = (string) $hexdec;
2607
2608 2
        if ($hexdec === '') {
2609 2
            return false;
2610
        }
2611
2612 2
        if (\preg_match('/^(?:\\\u|U\+|)([a-zA-Z0-9]{4,6})$/', $hexdec, $match)) {
2613 2
            return \intval($match[1], 16);
2614
        }
2615
2616 2
        return false;
2617
    }
2618
2619
    /**
2620
     * alias for "UTF8::html_entity_decode()"
2621
     *
2622
     * @param string $str
2623
     * @param int    $flags
2624
     * @param string $encoding
2625
     *
2626
     * @psalm-pure
2627
     *
2628
     * @return string
2629
     *
2630
     * @see        UTF8::html_entity_decode()
2631
     * @deprecated <p>please use "UTF8::html_entity_decode()"</p>
2632
     */
2633 2
    public static function html_decode(
2634
        string $str,
2635
        int $flags = null,
2636
        string $encoding = 'UTF-8'
2637
    ): string {
2638 2
        return self::html_entity_decode($str, $flags, $encoding);
2639
    }
2640
2641
    /**
2642
     * Converts a UTF-8 string to a series of HTML numbered entities.
2643
     *
2644
     * INFO: opposite to UTF8::html_decode()
2645
     *
2646
     * @param string $str              <p>The Unicode string to be encoded as numbered entities.</p>
2647
     * @param bool   $keep_ascii_chars [optional] <p>Keep ASCII chars.</p>
2648
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
2649
     *
2650
     * @psalm-pure
2651
     *
2652
     * @return string HTML numbered entities
2653
     */
2654 14
    public static function html_encode(
2655
        string $str,
2656
        bool $keep_ascii_chars = false,
2657
        string $encoding = 'UTF-8'
2658
    ): string {
2659 14
        if ($str === '') {
2660 4
            return '';
2661
        }
2662
2663 14
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2664 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2665
        }
2666
2667
        // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
2668 14
        if (self::$SUPPORT['mbstring'] === true) {
2669 14
            $start_code = 0x00;
2670 14
            if ($keep_ascii_chars) {
2671 13
                $start_code = 0x80;
2672
            }
2673
2674 14
            if ($encoding === 'UTF-8') {
2675
                /** @var false|string|null $return - needed for PhpStan (stubs error) */
2676 14
                $return = \mb_encode_numericentity(
2677 14
                    $str,
2678 14
                    [$start_code, 0xfffff, 0, 0xfffff, 0]
2679
                );
2680 14
                if ($return !== null && $return !== false) {
2681 14
                    return $return;
2682
                }
2683
            }
2684
2685
            /** @var false|string|null $return - needed for PhpStan (stubs error) */
2686 4
            $return = \mb_encode_numericentity(
2687 4
                $str,
2688 4
                [$start_code, 0xfffff, 0, 0xfffff, 0],
2689 4
                $encoding
2690
            );
2691 4
            if ($return !== null && $return !== false) {
2692 4
                return $return;
2693
            }
2694
        }
2695
2696
        //
2697
        // fallback via vanilla php
2698
        //
2699
2700
        return \implode(
2701
            '',
2702
            \array_map(
2703
                static function (string $chr) use ($keep_ascii_chars, $encoding): string {
2704
                    return self::single_chr_html_encode($chr, $keep_ascii_chars, $encoding);
2705
                },
2706
                self::str_split($str)
2707
            )
2708
        );
2709
    }
2710
2711
    /**
2712
     * UTF-8 version of html_entity_decode()
2713
     *
2714
     * The reason we are not using html_entity_decode() by itself is because
2715
     * while it is not technically correct to leave out the semicolon
2716
     * at the end of an entity most browsers will still interpret the entity
2717
     * correctly. html_entity_decode() does not convert entities without
2718
     * semicolons, so we are left with our own little solution here. Bummer.
2719
     *
2720
     * Convert all HTML entities to their applicable characters
2721
     *
2722
     * INFO: opposite to UTF8::html_encode()
2723
     *
2724
     * @see http://php.net/manual/en/function.html-entity-decode.php
2725
     *
2726
     * @param string $str      <p>
2727
     *                         The input string.
2728
     *                         </p>
2729
     * @param int    $flags    [optional] <p>
2730
     *                         A bitmask of one or more of the following flags, which specify how to handle quotes
2731
     *                         and which document type to use. The default is ENT_COMPAT | ENT_HTML401.
2732
     *                         <table>
2733
     *                         Available <i>flags</i> constants
2734
     *                         <tr valign="top">
2735
     *                         <td>Constant Name</td>
2736
     *                         <td>Description</td>
2737
     *                         </tr>
2738
     *                         <tr valign="top">
2739
     *                         <td><b>ENT_COMPAT</b></td>
2740
     *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
2741
     *                         </tr>
2742
     *                         <tr valign="top">
2743
     *                         <td><b>ENT_QUOTES</b></td>
2744
     *                         <td>Will convert both double and single quotes.</td>
2745
     *                         </tr>
2746
     *                         <tr valign="top">
2747
     *                         <td><b>ENT_NOQUOTES</b></td>
2748
     *                         <td>Will leave both double and single quotes unconverted.</td>
2749
     *                         </tr>
2750
     *                         <tr valign="top">
2751
     *                         <td><b>ENT_HTML401</b></td>
2752
     *                         <td>
2753
     *                         Handle code as HTML 4.01.
2754
     *                         </td>
2755
     *                         </tr>
2756
     *                         <tr valign="top">
2757
     *                         <td><b>ENT_XML1</b></td>
2758
     *                         <td>
2759
     *                         Handle code as XML 1.
2760
     *                         </td>
2761
     *                         </tr>
2762
     *                         <tr valign="top">
2763
     *                         <td><b>ENT_XHTML</b></td>
2764
     *                         <td>
2765
     *                         Handle code as XHTML.
2766
     *                         </td>
2767
     *                         </tr>
2768
     *                         <tr valign="top">
2769
     *                         <td><b>ENT_HTML5</b></td>
2770
     *                         <td>
2771
     *                         Handle code as HTML 5.
2772
     *                         </td>
2773
     *                         </tr>
2774
     *                         </table>
2775
     *                         </p>
2776
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2777
     *
2778
     * @psalm-pure
2779
     *
2780
     * @return string the decoded string
2781
     */
2782 51
    public static function html_entity_decode(
2783
        string $str,
2784
        int $flags = null,
2785
        string $encoding = 'UTF-8'
2786
    ): string {
2787
        if (
2788 51
            !isset($str[3]) // examples: &; || &x;
2789
            ||
2790 51
            \strpos($str, '&') === false // no "&"
2791
        ) {
2792 24
            return $str;
2793
        }
2794
2795 49
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2796 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2797
        }
2798
2799 49
        if ($flags === null) {
2800 11
            $flags = \ENT_QUOTES | \ENT_HTML5;
2801
        }
2802
2803
        if (
2804 49
            $encoding !== 'UTF-8'
2805
            &&
2806 49
            $encoding !== 'ISO-8859-1'
2807
            &&
2808 49
            $encoding !== 'WINDOWS-1252'
2809
            &&
2810 49
            self::$SUPPORT['mbstring'] === false
2811
        ) {
2812
            /**
2813
             * @psalm-suppress ImpureFunctionCall - is is only a warning
2814
             */
2815
            \trigger_error('UTF8::html_entity_decode() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
2816
        }
2817
2818
        do {
2819 49
            $str_compare = $str;
2820
2821 49
            if (\strpos($str, '&') !== false) {
2822 49
                if (\strpos($str, '&#') !== false) {
2823
                    // decode also numeric & UTF16 two byte entities
2824 41
                    $str = (string) \preg_replace(
2825 41
                        '/(&#(?:x0*[0-9a-fA-F]{2,6}(?![0-9a-fA-F;])|(?:0*\d{2,6}(?![0-9;]))))/S',
2826 41
                        '$1;',
2827 41
                        $str
2828
                    );
2829
                }
2830
2831 49
                $str = \html_entity_decode(
2832 49
                    $str,
2833 49
                    $flags,
2834 49
                    $encoding
2835
                );
2836
            }
2837 49
        } while ($str_compare !== $str);
2838
2839 49
        return $str;
2840
    }
2841
2842
    /**
2843
     * Create a escape html version of the string via "UTF8::htmlspecialchars()".
2844
     *
2845
     * @param string $str
2846
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2847
     *
2848
     * @psalm-pure
2849
     *
2850
     * @return string
2851
     */
2852 6
    public static function html_escape(string $str, string $encoding = 'UTF-8'): string
2853
    {
2854 6
        return self::htmlspecialchars(
2855 6
            $str,
2856 6
            \ENT_QUOTES | \ENT_SUBSTITUTE,
2857 6
            $encoding
2858
        );
2859
    }
2860
2861
    /**
2862
     * Remove empty html-tag.
2863
     *
2864
     * e.g.: <tag></tag>
2865
     *
2866
     * @param string $str
2867
     *
2868
     * @psalm-pure
2869
     *
2870
     * @return string
2871
     */
2872 1
    public static function html_stripe_empty_tags(string $str): string
2873
    {
2874 1
        return (string) \preg_replace(
2875 1
            '/<[^\\/>]*?>\\s*?<\\/[^>]*?>/u',
2876 1
            '',
2877 1
            $str
2878
        );
2879
    }
2880
2881
    /**
2882
     * Convert all applicable characters to HTML entities: UTF-8 version of htmlentities()
2883
     *
2884
     * @see http://php.net/manual/en/function.htmlentities.php
2885
     *
2886
     * @param string $str           <p>
2887
     *                              The input string.
2888
     *                              </p>
2889
     * @param int    $flags         [optional] <p>
2890
     *                              A bitmask of one or more of the following flags, which specify how to handle
2891
     *                              quotes, invalid code unit sequences and the used document type. The default is
2892
     *                              ENT_COMPAT | ENT_HTML401.
2893
     *                              <table>
2894
     *                              Available <i>flags</i> constants
2895
     *                              <tr valign="top">
2896
     *                              <td>Constant Name</td>
2897
     *                              <td>Description</td>
2898
     *                              </tr>
2899
     *                              <tr valign="top">
2900
     *                              <td><b>ENT_COMPAT</b></td>
2901
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2902
     *                              </tr>
2903
     *                              <tr valign="top">
2904
     *                              <td><b>ENT_QUOTES</b></td>
2905
     *                              <td>Will convert both double and single quotes.</td>
2906
     *                              </tr>
2907
     *                              <tr valign="top">
2908
     *                              <td><b>ENT_NOQUOTES</b></td>
2909
     *                              <td>Will leave both double and single quotes unconverted.</td>
2910
     *                              </tr>
2911
     *                              <tr valign="top">
2912
     *                              <td><b>ENT_IGNORE</b></td>
2913
     *                              <td>
2914
     *                              Silently discard invalid code unit sequences instead of returning
2915
     *                              an empty string. Using this flag is discouraged as it
2916
     *                              may have security implications.
2917
     *                              </td>
2918
     *                              </tr>
2919
     *                              <tr valign="top">
2920
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2921
     *                              <td>
2922
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
2923
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
2924
     *                              string.
2925
     *                              </td>
2926
     *                              </tr>
2927
     *                              <tr valign="top">
2928
     *                              <td><b>ENT_DISALLOWED</b></td>
2929
     *                              <td>
2930
     *                              Replace invalid code points for the given document type with a
2931
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
2932
     *                              (otherwise) instead of leaving them as is. This may be useful, for
2933
     *                              instance, to ensure the well-formedness of XML documents with
2934
     *                              embedded external content.
2935
     *                              </td>
2936
     *                              </tr>
2937
     *                              <tr valign="top">
2938
     *                              <td><b>ENT_HTML401</b></td>
2939
     *                              <td>
2940
     *                              Handle code as HTML 4.01.
2941
     *                              </td>
2942
     *                              </tr>
2943
     *                              <tr valign="top">
2944
     *                              <td><b>ENT_XML1</b></td>
2945
     *                              <td>
2946
     *                              Handle code as XML 1.
2947
     *                              </td>
2948
     *                              </tr>
2949
     *                              <tr valign="top">
2950
     *                              <td><b>ENT_XHTML</b></td>
2951
     *                              <td>
2952
     *                              Handle code as XHTML.
2953
     *                              </td>
2954
     *                              </tr>
2955
     *                              <tr valign="top">
2956
     *                              <td><b>ENT_HTML5</b></td>
2957
     *                              <td>
2958
     *                              Handle code as HTML 5.
2959
     *                              </td>
2960
     *                              </tr>
2961
     *                              </table>
2962
     *                              </p>
2963
     * @param string $encoding      [optional] <p>
2964
     *                              Like <b>htmlspecialchars</b>,
2965
     *                              <b>htmlentities</b> takes an optional third argument
2966
     *                              <i>encoding</i> which defines encoding used in
2967
     *                              conversion.
2968
     *                              Although this argument is technically optional, you are highly
2969
     *                              encouraged to specify the correct value for your code.
2970
     *                              </p>
2971
     * @param bool   $double_encode [optional] <p>
2972
     *                              When <i>double_encode</i> is turned off PHP will not
2973
     *                              encode existing html entities. The default is to convert everything.
2974
     *                              </p>
2975
     *
2976
     * @psalm-pure
2977
     *
2978
     * @return string
2979
     *                <p>
2980
     *                The encoded string.
2981
     *                <br><br>
2982
     *                If the input <i>string</i> contains an invalid code unit
2983
     *                sequence within the given <i>encoding</i> an empty string
2984
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
2985
     *                <b>ENT_SUBSTITUTE</b> flags are set.
2986
     *                </p>
2987
     */
2988 9
    public static function htmlentities(
2989
        string $str,
2990
        int $flags = \ENT_COMPAT,
2991
        string $encoding = 'UTF-8',
2992
        bool $double_encode = true
2993
    ): string {
2994 9
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2995 7
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2996
        }
2997
2998 9
        $str = \htmlentities(
2999 9
            $str,
3000 9
            $flags,
3001 9
            $encoding,
3002 9
            $double_encode
3003
        );
3004
3005
        /**
3006
         * PHP doesn't replace a backslash to its html entity since this is something
3007
         * that's mostly used to escape characters when inserting in a database. Since
3008
         * we're using a decent database layer, we don't need this shit and we're replacing
3009
         * the double backslashes by its' html entity equivalent.
3010
         *
3011
         * https://github.com/forkcms/library/blob/master/spoon/filter/filter.php#L303
3012
         */
3013 9
        $str = \str_replace('\\', '&#92;', $str);
3014
3015 9
        return self::html_encode($str, true, $encoding);
3016
    }
3017
3018
    /**
3019
     * Convert only special characters to HTML entities: UTF-8 version of htmlspecialchars()
3020
     *
3021
     * INFO: Take a look at "UTF8::htmlentities()"
3022
     *
3023
     * @see http://php.net/manual/en/function.htmlspecialchars.php
3024
     *
3025
     * @param string $str           <p>
3026
     *                              The string being converted.
3027
     *                              </p>
3028
     * @param int    $flags         [optional] <p>
3029
     *                              A bitmask of one or more of the following flags, which specify how to handle
3030
     *                              quotes, invalid code unit sequences and the used document type. The default is
3031
     *                              ENT_COMPAT | ENT_HTML401.
3032
     *                              <table>
3033
     *                              Available <i>flags</i> constants
3034
     *                              <tr valign="top">
3035
     *                              <td>Constant Name</td>
3036
     *                              <td>Description</td>
3037
     *                              </tr>
3038
     *                              <tr valign="top">
3039
     *                              <td><b>ENT_COMPAT</b></td>
3040
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
3041
     *                              </tr>
3042
     *                              <tr valign="top">
3043
     *                              <td><b>ENT_QUOTES</b></td>
3044
     *                              <td>Will convert both double and single quotes.</td>
3045
     *                              </tr>
3046
     *                              <tr valign="top">
3047
     *                              <td><b>ENT_NOQUOTES</b></td>
3048
     *                              <td>Will leave both double and single quotes unconverted.</td>
3049
     *                              </tr>
3050
     *                              <tr valign="top">
3051
     *                              <td><b>ENT_IGNORE</b></td>
3052
     *                              <td>
3053
     *                              Silently discard invalid code unit sequences instead of returning
3054
     *                              an empty string. Using this flag is discouraged as it
3055
     *                              may have security implications.
3056
     *                              </td>
3057
     *                              </tr>
3058
     *                              <tr valign="top">
3059
     *                              <td><b>ENT_SUBSTITUTE</b></td>
3060
     *                              <td>
3061
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
3062
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
3063
     *                              string.
3064
     *                              </td>
3065
     *                              </tr>
3066
     *                              <tr valign="top">
3067
     *                              <td><b>ENT_DISALLOWED</b></td>
3068
     *                              <td>
3069
     *                              Replace invalid code points for the given document type with a
3070
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
3071
     *                              (otherwise) instead of leaving them as is. This may be useful, for
3072
     *                              instance, to ensure the well-formedness of XML documents with
3073
     *                              embedded external content.
3074
     *                              </td>
3075
     *                              </tr>
3076
     *                              <tr valign="top">
3077
     *                              <td><b>ENT_HTML401</b></td>
3078
     *                              <td>
3079
     *                              Handle code as HTML 4.01.
3080
     *                              </td>
3081
     *                              </tr>
3082
     *                              <tr valign="top">
3083
     *                              <td><b>ENT_XML1</b></td>
3084
     *                              <td>
3085
     *                              Handle code as XML 1.
3086
     *                              </td>
3087
     *                              </tr>
3088
     *                              <tr valign="top">
3089
     *                              <td><b>ENT_XHTML</b></td>
3090
     *                              <td>
3091
     *                              Handle code as XHTML.
3092
     *                              </td>
3093
     *                              </tr>
3094
     *                              <tr valign="top">
3095
     *                              <td><b>ENT_HTML5</b></td>
3096
     *                              <td>
3097
     *                              Handle code as HTML 5.
3098
     *                              </td>
3099
     *                              </tr>
3100
     *                              </table>
3101
     *                              </p>
3102
     * @param string $encoding      [optional] <p>
3103
     *                              Defines encoding used in conversion.
3104
     *                              </p>
3105
     *                              <p>
3106
     *                              For the purposes of this function, the encodings
3107
     *                              ISO-8859-1, ISO-8859-15,
3108
     *                              UTF-8, cp866,
3109
     *                              cp1251, cp1252, and
3110
     *                              KOI8-R are effectively equivalent, provided the
3111
     *                              <i>string</i> itself is valid for the encoding, as
3112
     *                              the characters affected by <b>htmlspecialchars</b> occupy
3113
     *                              the same positions in all of these encodings.
3114
     *                              </p>
3115
     * @param bool   $double_encode [optional] <p>
3116
     *                              When <i>double_encode</i> is turned off PHP will not
3117
     *                              encode existing html entities, the default is to convert everything.
3118
     *                              </p>
3119
     *
3120
     * @psalm-pure
3121
     *
3122
     * @return string the converted string.
3123
     *                </p>
3124
     *                <p>
3125
     *                If the input <i>string</i> contains an invalid code unit
3126
     *                sequence within the given <i>encoding</i> an empty string
3127
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
3128
     *                <b>ENT_SUBSTITUTE</b> flags are set
3129
     */
3130 8
    public static function htmlspecialchars(
3131
        string $str,
3132
        int $flags = \ENT_COMPAT,
3133
        string $encoding = 'UTF-8',
3134
        bool $double_encode = true
3135
    ): string {
3136 8
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
3137 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
3138
        }
3139
3140 8
        return \htmlspecialchars(
3141 8
            $str,
3142 8
            $flags,
3143 8
            $encoding,
3144 8
            $double_encode
3145
        );
3146
    }
3147
3148
    /**
3149
     * Checks whether iconv is available on the server.
3150
     *
3151
     * @psalm-pure
3152
     *
3153
     * @return bool
3154
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3155
     */
3156
    public static function iconv_loaded(): bool
3157
    {
3158
        return \extension_loaded('iconv');
3159
    }
3160
3161
    /**
3162
     * alias for "UTF8::decimal_to_chr()"
3163
     *
3164
     * @param mixed $int
3165
     *
3166
     * @psalm-pure
3167
     *
3168
     * @return string
3169
     *
3170
     * @see        UTF8::decimal_to_chr()
3171
     * @deprecated <p>please use "UTF8::decimal_to_chr()"</p>
3172
     */
3173 4
    public static function int_to_chr($int): string
3174
    {
3175 4
        return self::decimal_to_chr($int);
3176
    }
3177
3178
    /**
3179
     * Converts Integer to hexadecimal U+xxxx code point representation.
3180
     *
3181
     * INFO: opposite to UTF8::hex_to_int()
3182
     *
3183
     * @param int    $int    <p>The integer to be converted to hexadecimal code point.</p>
3184
     * @param string $prefix [optional]
3185
     *
3186
     * @psalm-pure
3187
     *
3188
     * @return string the code point, or empty string on failure
3189
     */
3190 6
    public static function int_to_hex(int $int, string $prefix = 'U+'): string
3191
    {
3192 6
        $hex = \dechex($int);
3193
3194 6
        $hex = (\strlen($hex) < 4 ? \substr('0000' . $hex, -4) : $hex);
3195
3196 6
        return $prefix . $hex . '';
3197
    }
3198
3199
    /**
3200
     * Checks whether intl-char is available on the server.
3201
     *
3202
     * @psalm-pure
3203
     *
3204
     * @return bool
3205
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3206
     */
3207
    public static function intlChar_loaded(): bool
3208
    {
3209
        return \class_exists('IntlChar');
3210
    }
3211
3212
    /**
3213
     * Checks whether intl is available on the server.
3214
     *
3215
     * @psalm-pure
3216
     *
3217
     * @return bool
3218
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3219
     */
3220 5
    public static function intl_loaded(): bool
3221
    {
3222 5
        return \extension_loaded('intl');
3223
    }
3224
3225
    /**
3226
     * alias for "UTF8::is_ascii()"
3227
     *
3228
     * @param string $str
3229
     *
3230
     * @psalm-pure
3231
     *
3232
     * @return bool
3233
     *
3234
     * @see        UTF8::is_ascii()
3235
     * @deprecated <p>please use "UTF8::is_ascii()"</p>
3236
     */
3237 2
    public static function isAscii(string $str): bool
3238
    {
3239 2
        return ASCII::is_ascii($str);
3240
    }
3241
3242
    /**
3243
     * alias for "UTF8::is_base64()"
3244
     *
3245
     * @param string $str
3246
     *
3247
     * @psalm-pure
3248
     *
3249
     * @return bool
3250
     *
3251
     * @see        UTF8::is_base64()
3252
     * @deprecated <p>please use "UTF8::is_base64()"</p>
3253
     */
3254 2
    public static function isBase64($str): bool
3255
    {
3256 2
        return self::is_base64($str);
3257
    }
3258
3259
    /**
3260
     * alias for "UTF8::is_binary()"
3261
     *
3262
     * @param mixed $str
3263
     * @param bool  $strict
3264
     *
3265
     * @psalm-pure
3266
     *
3267
     * @return bool
3268
     *
3269
     * @see        UTF8::is_binary()
3270
     * @deprecated <p>please use "UTF8::is_binary()"</p>
3271
     */
3272 4
    public static function isBinary($str, bool $strict = false): bool
3273
    {
3274 4
        return self::is_binary($str, $strict);
3275
    }
3276
3277
    /**
3278
     * alias for "UTF8::is_bom()"
3279
     *
3280
     * @param string $utf8_chr
3281
     *
3282
     * @psalm-pure
3283
     *
3284
     * @return bool
3285
     *
3286
     * @see        UTF8::is_bom()
3287
     * @deprecated <p>please use "UTF8::is_bom()"</p>
3288
     */
3289 2
    public static function isBom(string $utf8_chr): bool
3290
    {
3291 2
        return self::is_bom($utf8_chr);
3292
    }
3293
3294
    /**
3295
     * alias for "UTF8::is_html()"
3296
     *
3297
     * @param string $str
3298
     *
3299
     * @psalm-pure
3300
     *
3301
     * @return bool
3302
     *
3303
     * @see        UTF8::is_html()
3304
     * @deprecated <p>please use "UTF8::is_html()"</p>
3305
     */
3306 2
    public static function isHtml(string $str): bool
3307
    {
3308 2
        return self::is_html($str);
3309
    }
3310
3311
    /**
3312
     * alias for "UTF8::is_json()"
3313
     *
3314
     * @param string $str
3315
     *
3316
     * @return bool
3317
     *
3318
     * @see        UTF8::is_json()
3319
     * @deprecated <p>please use "UTF8::is_json()"</p>
3320
     */
3321 1
    public static function isJson(string $str): bool
3322
    {
3323 1
        return self::is_json($str);
3324
    }
3325
3326
    /**
3327
     * alias for "UTF8::is_utf16()"
3328
     *
3329
     * @param mixed $str
3330
     *
3331
     * @psalm-pure
3332
     *
3333
     * @return false|int
3334
     *                   <strong>false</strong> if is't not UTF16,<br>
3335
     *                   <strong>1</strong> for UTF-16LE,<br>
3336
     *                   <strong>2</strong> for UTF-16BE
3337
     *
3338
     * @see        UTF8::is_utf16()
3339
     * @deprecated <p>please use "UTF8::is_utf16()"</p>
3340
     */
3341 2
    public static function isUtf16($str)
3342
    {
3343 2
        return self::is_utf16($str);
3344
    }
3345
3346
    /**
3347
     * alias for "UTF8::is_utf32()"
3348
     *
3349
     * @param mixed $str
3350
     *
3351
     * @psalm-pure
3352
     *
3353
     * @return false|int
3354
     *                   <strong>false</strong> if is't not UTF16,
3355
     *                   <strong>1</strong> for UTF-32LE,
3356
     *                   <strong>2</strong> for UTF-32BE
3357
     *
3358
     * @see        UTF8::is_utf32()
3359
     * @deprecated <p>please use "UTF8::is_utf32()"</p>
3360
     */
3361 2
    public static function isUtf32($str)
3362
    {
3363 2
        return self::is_utf32($str);
3364
    }
3365
3366
    /**
3367
     * alias for "UTF8::is_utf8()"
3368
     *
3369
     * @param string $str
3370
     * @param bool   $strict
3371
     *
3372
     * @psalm-pure
3373
     *
3374
     * @return bool
3375
     *
3376
     * @see        UTF8::is_utf8()
3377
     * @deprecated <p>please use "UTF8::is_utf8()"</p>
3378
     */
3379 17
    public static function isUtf8($str, bool $strict = false): bool
3380
    {
3381 17
        return self::is_utf8($str, $strict);
3382
    }
3383
3384
    /**
3385
     * Returns true if the string contains only alphabetic chars, false otherwise.
3386
     *
3387
     * @param string $str <p>The input string.</p>
3388
     *
3389
     * @psalm-pure
3390
     *
3391
     * @return bool
3392
     *              <p>Whether or not $str contains only alphabetic chars.</p>
3393
     */
3394 10
    public static function is_alpha(string $str): bool
3395
    {
3396 10
        if (self::$SUPPORT['mbstring'] === true) {
3397
            /** @noinspection PhpComposerExtensionStubsInspection */
3398 10
            return \mb_ereg_match('^[[:alpha:]]*$', $str);
3399
        }
3400
3401
        return self::str_matches_pattern($str, '^[[:alpha:]]*$');
3402
    }
3403
3404
    /**
3405
     * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
3406
     *
3407
     * @param string $str <p>The input string.</p>
3408
     *
3409
     * @psalm-pure
3410
     *
3411
     * @return bool
3412
     *              <p>Whether or not $str contains only alphanumeric chars.</p>
3413
     */
3414 13
    public static function is_alphanumeric(string $str): bool
3415
    {
3416 13
        if (self::$SUPPORT['mbstring'] === true) {
3417
            /** @noinspection PhpComposerExtensionStubsInspection */
3418 13
            return \mb_ereg_match('^[[:alnum:]]*$', $str);
3419
        }
3420
3421
        return self::str_matches_pattern($str, '^[[:alnum:]]*$');
3422
    }
3423
3424
    /**
3425
     * Returns true if the string contains only punctuation chars, false otherwise.
3426
     *
3427
     * @param string $str <p>The input string.</p>
3428
     *
3429
     * @psalm-pure
3430
     *
3431
     * @return bool
3432
     *              <p>Whether or not $str contains only punctuation chars.</p>
3433
     */
3434 10
    public static function is_punctuation(string $str): bool
3435
    {
3436 10
        return self::str_matches_pattern($str, '^[[:punct:]]*$');
3437
    }
3438
3439
    /**
3440
     * Returns true if the string contains only printable (non-invisible) chars, false otherwise.
3441
     *
3442
     * @param string $str <p>The input string.</p>
3443
     *
3444
     * @psalm-pure
3445
     *
3446
     * @return bool
3447
     *              <p>Whether or not $str contains only printable (non-invisible) chars.</p>
3448
     */
3449 1
    public static function is_printable(string $str): bool
3450
    {
3451 1
        return self::remove_invisible_characters($str) === $str;
3452
    }
3453
3454
    /**
3455
     * Checks if a string is 7 bit ASCII.
3456
     *
3457
     * @param string $str <p>The string to check.</p>
3458
     *
3459
     * @psalm-pure
3460
     *
3461
     * @return bool
3462
     *              <p>
3463
     *              <strong>true</strong> if it is ASCII<br>
3464
     *              <strong>false</strong> otherwise
3465
     *              </p>
3466
     */
3467 8
    public static function is_ascii(string $str): bool
3468
    {
3469 8
        return ASCII::is_ascii($str);
3470
    }
3471
3472
    /**
3473
     * Returns true if the string is base64 encoded, false otherwise.
3474
     *
3475
     * @param mixed|string $str                   <p>The input string.</p>
3476
     * @param bool         $empty_string_is_valid [optional] <p>Is an empty string valid base64 or not?</p>
3477
     *
3478
     * @psalm-pure
3479
     *
3480
     * @return bool
3481
     *              <p>Whether or not $str is base64 encoded.</p>
3482
     */
3483 16
    public static function is_base64($str, bool $empty_string_is_valid = false): bool
3484
    {
3485
        if (
3486 16
            !$empty_string_is_valid
3487
            &&
3488 16
            $str === ''
3489
        ) {
3490 3
            return false;
3491
        }
3492
3493
        /**
3494
         * @psalm-suppress RedundantConditionGivenDocblockType
3495
         */
3496 15
        if (!\is_string($str)) {
3497 2
            return false;
3498
        }
3499
3500 15
        $base64String = \base64_decode($str, true);
3501
3502 15
        return $base64String !== false && \base64_encode($base64String) === $str;
3503
    }
3504
3505
    /**
3506
     * Check if the input is binary... (is look like a hack).
3507
     *
3508
     * @param mixed $input
3509
     * @param bool  $strict
3510
     *
3511
     * @psalm-pure
3512
     *
3513
     * @return bool
3514
     */
3515 39
    public static function is_binary($input, bool $strict = false): bool
3516
    {
3517 39
        $input = (string) $input;
3518 39
        if ($input === '') {
3519 10
            return false;
3520
        }
3521
3522 39
        if (\preg_match('~^[01]+$~', $input)) {
3523 13
            return true;
3524
        }
3525
3526 39
        $ext = self::get_file_type($input);
3527 39
        if ($ext['type'] === 'binary') {
3528 7
            return true;
3529
        }
3530
3531 38
        $test_length = \strlen($input);
3532 38
        $test_null_counting = \substr_count($input, "\x0", 0, $test_length);
3533 38
        if (($test_null_counting / $test_length) > 0.25) {
3534 15
            return true;
3535
        }
3536
3537 34
        if ($strict) {
3538 34
            if (self::$SUPPORT['finfo'] === false) {
3539
                throw new \RuntimeException('ext-fileinfo: is not installed');
3540
            }
3541
3542
            /**
3543
             * @noinspection   PhpComposerExtensionStubsInspection
3544
             * @psalm-suppress ImpureMethodCall - it will return the same result for the same file ...
3545
             */
3546 34
            $finfo_encoding = (new \finfo(\FILEINFO_MIME_ENCODING))->buffer($input);
3547 34
            if ($finfo_encoding && $finfo_encoding === 'binary') {
3548 15
                return true;
3549
            }
3550
        }
3551
3552 30
        return false;
3553
    }
3554
3555
    /**
3556
     * Check if the file is binary.
3557
     *
3558
     * @param string $file
3559
     *
3560
     * @return bool
3561
     */
3562 6
    public static function is_binary_file($file): bool
3563
    {
3564
        // init
3565 6
        $block = '';
3566
3567 6
        $fp = \fopen($file, 'rb');
3568 6
        if (\is_resource($fp)) {
3569 6
            $block = \fread($fp, 512);
3570 6
            \fclose($fp);
3571
        }
3572
3573 6
        if ($block === '') {
3574 2
            return false;
3575
        }
3576
3577 6
        return self::is_binary($block, true);
3578
    }
3579
3580
    /**
3581
     * Returns true if the string contains only whitespace chars, false otherwise.
3582
     *
3583
     * @param string $str <p>The input string.</p>
3584
     *
3585
     * @psalm-pure
3586
     *
3587
     * @return bool
3588
     *              <p>Whether or not $str contains only whitespace characters.</p>
3589
     */
3590 15
    public static function is_blank(string $str): bool
3591
    {
3592 15
        if (self::$SUPPORT['mbstring'] === true) {
3593
            /** @noinspection PhpComposerExtensionStubsInspection */
3594 15
            return \mb_ereg_match('^[[:space:]]*$', $str);
3595
        }
3596
3597
        return self::str_matches_pattern($str, '^[[:space:]]*$');
3598
    }
3599
3600
    /**
3601
     * Checks if the given string is equal to any "Byte Order Mark".
3602
     *
3603
     * WARNING: Use "UTF8::string_has_bom()" if you will check BOM in a string.
3604
     *
3605
     * @param string $str <p>The input string.</p>
3606
     *
3607
     * @psalm-pure
3608
     *
3609
     * @return bool
3610
     *              <p><strong>true</strong> if the $utf8_chr is Byte Order Mark, <strong>false</strong> otherwise.</p>
3611
     */
3612 2
    public static function is_bom($str): bool
3613
    {
3614
        /** @noinspection PhpUnusedLocalVariableInspection */
3615 2
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
3616 2
            if ($str === $bom_string) {
3617 2
                return true;
3618
            }
3619
        }
3620
3621 2
        return false;
3622
    }
3623
3624
    /**
3625
     * Determine whether the string is considered to be empty.
3626
     *
3627
     * A variable is considered empty if it does not exist or if its value equals FALSE.
3628
     * empty() does not generate a warning if the variable does not exist.
3629
     *
3630
     * @param mixed $str
3631
     *
3632
     * @psalm-pure
3633
     *
3634
     * @return bool
3635
     *              <p>Whether or not $str is empty().</p>
3636
     */
3637 1
    public static function is_empty($str): bool
3638
    {
3639 1
        return empty($str);
3640
    }
3641
3642
    /**
3643
     * Returns true if the string contains only hexadecimal chars, false otherwise.
3644
     *
3645
     * @param string $str <p>The input string.</p>
3646
     *
3647
     * @psalm-pure
3648
     *
3649
     * @return bool
3650
     *              <p>Whether or not $str contains only hexadecimal chars.</p>
3651
     */
3652 13
    public static function is_hexadecimal(string $str): bool
3653
    {
3654 13
        if (self::$SUPPORT['mbstring'] === true) {
3655
            /** @noinspection PhpComposerExtensionStubsInspection */
3656 13
            return \mb_ereg_match('^[[:xdigit:]]*$', $str);
3657
        }
3658
3659
        return self::str_matches_pattern($str, '^[[:xdigit:]]*$');
3660
    }
3661
3662
    /**
3663
     * Check if the string contains any HTML tags.
3664
     *
3665
     * @param string $str <p>The input string.</p>
3666
     *
3667
     * @psalm-pure
3668
     *
3669
     * @return bool
3670
     *              <p>Whether or not $str contains html elements.</p>
3671
     */
3672 3
    public static function is_html(string $str): bool
3673
    {
3674 3
        if ($str === '') {
3675 3
            return false;
3676
        }
3677
3678
        // init
3679 3
        $matches = [];
3680
3681 3
        $str = self::emoji_encode($str); // hack for emoji support :/
3682
3683 3
        \preg_match("/<\\/?\\w+(?:(?:\\s+\\w+(?:\\s*=\\s*(?:\".*?\"|'.*?'|[^'\">\\s]+))?)*\\s*|\\s*)\\/?>/u", $str, $matches);
3684
3685 3
        return $matches !== [];
3686
    }
3687
3688
    /**
3689
     * Check if $url is an correct url.
3690
     *
3691
     * @param string $url
3692
     * @param bool   $disallow_localhost
3693
     *
3694
     * @psalm-pure
3695
     *
3696
     * @return bool
3697
     */
3698 1
    public static function is_url(string $url, bool $disallow_localhost = false): bool
3699
    {
3700 1
        if ($url === '') {
3701 1
            return false;
3702
        }
3703
3704
        // WARNING: keep this as hack protection
3705 1
        if (!self::str_istarts_with_any($url, ['http://', 'https://'])) {
3706 1
            return false;
3707
        }
3708
3709
        // e.g. -> the server itself connect to "https://foo.localhost/phpmyadmin/...
3710 1
        if ($disallow_localhost) {
3711 1
            if (self::str_istarts_with_any(
3712 1
                $url,
3713
                [
3714 1
                    'http://localhost',
3715
                    'https://localhost',
3716
                    'http://127.0.0.1',
3717
                    'https://127.0.0.1',
3718
                    'http://::1',
3719
                    'https://::1',
3720
                ]
3721
            )) {
3722 1
                return false;
3723
            }
3724
3725 1
            $regex = '/^(?:http(?:s)?:\/\/).*?(?:\.localhost)/iu';
3726
            /** @noinspection BypassedUrlValidationInspection */
3727 1
            if (\preg_match($regex, $url)) {
3728 1
                return false;
3729
            }
3730
        }
3731
3732
        // INFO: this is needed for e.g. "http://müller.de/" (internationalized domain names) and non ASCII-parameters
3733
        /** @noinspection SuspiciousAssignmentsInspection - false-positive - https://github.com/kalessil/phpinspectionsea/issues/1500 */
3734 1
        $regex = '/^(?:http(?:s)?:\\/\\/)(?:[\p{L}0-9][\p{L}0-9_-]*(?:\\.[\p{L}0-9][\p{L}0-9_-]*))(?:\\d+)?(?:\\/\\.*)?/iu';
3735
        /** @noinspection BypassedUrlValidationInspection */
3736 1
        if (\preg_match($regex, $url)) {
3737 1
            return true;
3738
        }
3739
3740
        /** @noinspection BypassedUrlValidationInspection */
3741 1
        return \filter_var($url, \FILTER_VALIDATE_URL) !== false;
3742
    }
3743
3744
    /**
3745
     * Try to check if "$str" is a JSON-string.
3746
     *
3747
     * @param string $str                                    <p>The input string.</p>
3748
     * @param bool   $only_array_or_object_results_are_valid [optional] <p>Only array and objects are valid json
3749
     *                                                       results.</p>
3750
     *
3751
     * @return bool
3752
     *              <p>Whether or not the $str is in JSON format.</p>
3753
     */
3754 42
    public static function is_json(string $str, bool $only_array_or_object_results_are_valid = true): bool
3755
    {
3756 42
        if ($str === '') {
3757 4
            return false;
3758
        }
3759
3760 40
        if (self::$SUPPORT['json'] === false) {
3761
            throw new \RuntimeException('ext-json: is not installed');
3762
        }
3763
3764 40
        $jsonOrNull = self::json_decode($str);
3765 40
        if ($jsonOrNull === null && \strtoupper($str) !== 'NULL') {
3766 18
            return false;
3767
        }
3768
3769
        if (
3770 24
            $only_array_or_object_results_are_valid
3771
            &&
3772 24
            !\is_object($jsonOrNull)
3773
            &&
3774 24
            !\is_array($jsonOrNull)
3775
        ) {
3776 5
            return false;
3777
        }
3778
3779
        /** @noinspection PhpComposerExtensionStubsInspection */
3780 19
        return \json_last_error() === \JSON_ERROR_NONE;
3781
    }
3782
3783
    /**
3784
     * @param string $str <p>The input string.</p>
3785
     *
3786
     * @psalm-pure
3787
     *
3788
     * @return bool
3789
     *              <p>Whether or not $str contains only lowercase chars.</p>
3790
     */
3791 8
    public static function is_lowercase(string $str): bool
3792
    {
3793 8
        if (self::$SUPPORT['mbstring'] === true) {
3794
            /** @noinspection PhpComposerExtensionStubsInspection */
3795 8
            return \mb_ereg_match('^[[:lower:]]*$', $str);
3796
        }
3797
3798
        return self::str_matches_pattern($str, '^[[:lower:]]*$');
3799
    }
3800
3801
    /**
3802
     * Returns true if the string is serialized, false otherwise.
3803
     *
3804
     * @param string $str <p>The input string.</p>
3805
     *
3806
     * @psalm-pure
3807
     *
3808
     * @return bool
3809
     *              <p>Whether or not $str is serialized.</p>
3810
     */
3811 7
    public static function is_serialized(string $str): bool
3812
    {
3813 7
        if ($str === '') {
3814 1
            return false;
3815
        }
3816
3817
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
3818
        /** @noinspection UnserializeExploitsInspection */
3819 6
        return $str === 'b:0;'
3820
               ||
3821 6
               @\unserialize($str) !== false;
3822
    }
3823
3824
    /**
3825
     * Returns true if the string contains only lower case chars, false
3826
     * otherwise.
3827
     *
3828
     * @param string $str <p>The input string.</p>
3829
     *
3830
     * @psalm-pure
3831
     *
3832
     * @return bool
3833
     *              <p>Whether or not $str contains only lower case characters.</p>
3834
     */
3835 8
    public static function is_uppercase(string $str): bool
3836
    {
3837 8
        if (self::$SUPPORT['mbstring'] === true) {
3838
            /** @noinspection PhpComposerExtensionStubsInspection */
3839 8
            return \mb_ereg_match('^[[:upper:]]*$', $str);
3840
        }
3841
3842
        return self::str_matches_pattern($str, '^[[:upper:]]*$');
3843
    }
3844
3845
    /**
3846
     * Check if the string is UTF-16.
3847
     *
3848
     * @param mixed $str                       <p>The input string.</p>
3849
     * @param bool  $check_if_string_is_binary
3850
     *
3851
     * @psalm-pure
3852
     *
3853
     * @return false|int
3854
     *                   <strong>false</strong> if is't not UTF-16,<br>
3855
     *                   <strong>1</strong> for UTF-16LE,<br>
3856
     *                   <strong>2</strong> for UTF-16BE
3857
     */
3858 22
    public static function is_utf16($str, bool $check_if_string_is_binary = true)
3859
    {
3860
        // init
3861 22
        $str = (string) $str;
3862 22
        $str_chars = [];
3863
3864
        if (
3865 22
            $check_if_string_is_binary
3866
            &&
3867 22
            !self::is_binary($str, true)
3868
        ) {
3869 2
            return false;
3870
        }
3871
3872 22
        if (self::$SUPPORT['mbstring'] === false) {
3873
            /**
3874
             * @psalm-suppress ImpureFunctionCall - is is only a warning
3875
             */
3876 3
            \trigger_error('UTF8::is_utf16() without mbstring may did not work correctly', \E_USER_WARNING);
3877
        }
3878
3879 22
        $str = self::remove_bom($str);
3880
3881 22
        $maybe_utf16le = 0;
3882 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16LE');
3883 22
        if ($test) {
3884 15
            $test2 = \mb_convert_encoding($test, 'UTF-16LE', 'UTF-8');
3885 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16LE');
3886 15
            if ($test3 === $test) {
3887
                /**
3888
                 * @psalm-suppress RedundantCondition
3889
                 */
3890 15
                if ($str_chars === []) {
3891 15
                    $str_chars = self::count_chars($str, true, false);
3892
                }
3893 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...
3894 15
                    if (\in_array($test3char, $str_chars, true)) {
3895 15
                        ++$maybe_utf16le;
3896
                    }
3897
                }
3898 15
                unset($test3charEmpty);
3899
            }
3900
        }
3901
3902 22
        $maybe_utf16be = 0;
3903 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16BE');
3904 22
        if ($test) {
3905 15
            $test2 = \mb_convert_encoding($test, 'UTF-16BE', 'UTF-8');
3906 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16BE');
3907 15
            if ($test3 === $test) {
3908 15
                if ($str_chars === []) {
3909 7
                    $str_chars = self::count_chars($str, true, false);
3910
                }
3911 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...
3912 15
                    if (\in_array($test3char, $str_chars, true)) {
3913 15
                        ++$maybe_utf16be;
3914
                    }
3915
                }
3916 15
                unset($test3charEmpty);
3917
            }
3918
        }
3919
3920 22
        if ($maybe_utf16be !== $maybe_utf16le) {
3921 7
            if ($maybe_utf16le > $maybe_utf16be) {
3922 5
                return 1;
3923
            }
3924
3925 6
            return 2;
3926
        }
3927
3928 18
        return false;
3929
    }
3930
3931
    /**
3932
     * Check if the string is UTF-32.
3933
     *
3934
     * @param mixed $str                       <p>The input string.</p>
3935
     * @param bool  $check_if_string_is_binary
3936
     *
3937
     * @psalm-pure
3938
     *
3939
     * @return false|int
3940
     *                   <strong>false</strong> if is't not UTF-32,<br>
3941
     *                   <strong>1</strong> for UTF-32LE,<br>
3942
     *                   <strong>2</strong> for UTF-32BE
3943
     */
3944 20
    public static function is_utf32($str, bool $check_if_string_is_binary = true)
3945
    {
3946
        // init
3947 20
        $str = (string) $str;
3948 20
        $str_chars = [];
3949
3950
        if (
3951 20
            $check_if_string_is_binary
3952
            &&
3953 20
            !self::is_binary($str, true)
3954
        ) {
3955 2
            return false;
3956
        }
3957
3958 20
        if (self::$SUPPORT['mbstring'] === false) {
3959
            /**
3960
             * @psalm-suppress ImpureFunctionCall - is is only a warning
3961
             */
3962 3
            \trigger_error('UTF8::is_utf32() without mbstring may did not work correctly', \E_USER_WARNING);
3963
        }
3964
3965 20
        $str = self::remove_bom($str);
3966
3967 20
        $maybe_utf32le = 0;
3968 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32LE');
3969 20
        if ($test) {
3970 13
            $test2 = \mb_convert_encoding($test, 'UTF-32LE', 'UTF-8');
3971 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32LE');
3972 13
            if ($test3 === $test) {
3973
                /**
3974
                 * @psalm-suppress RedundantCondition
3975
                 */
3976 13
                if ($str_chars === []) {
3977 13
                    $str_chars = self::count_chars($str, true, false);
3978
                }
3979 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...
3980 13
                    if (\in_array($test3char, $str_chars, true)) {
3981 13
                        ++$maybe_utf32le;
3982
                    }
3983
                }
3984 13
                unset($test3charEmpty);
3985
            }
3986
        }
3987
3988 20
        $maybe_utf32be = 0;
3989 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32BE');
3990 20
        if ($test) {
3991 13
            $test2 = \mb_convert_encoding($test, 'UTF-32BE', 'UTF-8');
3992 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32BE');
3993 13
            if ($test3 === $test) {
3994 13
                if ($str_chars === []) {
3995 7
                    $str_chars = self::count_chars($str, true, false);
3996
                }
3997 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...
3998 13
                    if (\in_array($test3char, $str_chars, true)) {
3999 13
                        ++$maybe_utf32be;
4000
                    }
4001
                }
4002 13
                unset($test3charEmpty);
4003
            }
4004
        }
4005
4006 20
        if ($maybe_utf32be !== $maybe_utf32le) {
4007 3
            if ($maybe_utf32le > $maybe_utf32be) {
4008 2
                return 1;
4009
            }
4010
4011 3
            return 2;
4012
        }
4013
4014 20
        return false;
4015
    }
4016
4017
    /**
4018
     * Checks whether the passed input contains only byte sequences that appear valid UTF-8.
4019
     *
4020
     * @param int|string|string[]|null $str    <p>The input to be checked.</p>
4021
     * @param bool                     $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
4022
     *
4023
     * @psalm-pure
4024
     *
4025
     * @return bool
4026
     */
4027 82
    public static function is_utf8($str, bool $strict = false): bool
4028
    {
4029 82
        if (\is_array($str)) {
4030 2
            foreach ($str as &$v) {
4031 2
                if (!self::is_utf8($v, $strict)) {
4032 2
                    return false;
4033
                }
4034
            }
4035
4036
            return true;
4037
        }
4038
4039 82
        return self::is_utf8_string((string) $str, $strict);
4040
    }
4041
4042
    /**
4043
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
4044
     * Decodes a JSON string
4045
     *
4046
     * @see http://php.net/manual/en/function.json-decode.php
4047
     *
4048
     * @param string $json    <p>
4049
     *                        The <i>json</i> string being decoded.
4050
     *                        </p>
4051
     *                        <p>
4052
     *                        This function only works with UTF-8 encoded strings.
4053
     *                        </p>
4054
     *                        <p>PHP implements a superset of
4055
     *                        JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
4056
     *                        only supports these values when they are nested inside an array or an object.
4057
     *                        </p>
4058
     * @param bool   $assoc   [optional] <p>
4059
     *                        When <b>TRUE</b>, returned objects will be converted into
4060
     *                        associative arrays.
4061
     *                        </p>
4062
     * @param int    $depth   [optional] <p>
4063
     *                        User specified recursion depth.
4064
     *                        </p>
4065
     * @param int    $options [optional] <p>
4066
     *                        Bitmask of JSON decode options. Currently only
4067
     *                        <b>JSON_BIGINT_AS_STRING</b>
4068
     *                        is supported (default is to cast large integers as floats)
4069
     *                        </p>
4070
     *
4071
     * @psalm-pure
4072
     *
4073
     * @return mixed
4074
     *               The value encoded in <i>json</i> in appropriate PHP type. Values true, false and
4075
     *               null (case-insensitive) are returned as <b>TRUE</b>, <b>FALSE</b> and <b>NULL</b> respectively.
4076
     *               <b>NULL</b> is returned if the <i>json</i> cannot be decoded or if the encoded data
4077
     *               is deeper than the recursion limit.
4078
     */
4079 43
    public static function json_decode(
4080
        string $json,
4081
        bool $assoc = false,
4082
        int $depth = 512,
4083
        int $options = 0
4084
    ) {
4085 43
        $json = self::filter($json);
4086
4087 43
        if (self::$SUPPORT['json'] === false) {
4088
            throw new \RuntimeException('ext-json: is not installed');
4089
        }
4090
4091
        /** @noinspection PhpComposerExtensionStubsInspection */
4092 43
        return \json_decode($json, $assoc, $depth, $options);
4093
    }
4094
4095
    /**
4096
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
4097
     * Returns the JSON representation of a value.
4098
     *
4099
     * @see http://php.net/manual/en/function.json-encode.php
4100
     *
4101
     * @param mixed $value   <p>
4102
     *                       The <i>value</i> being encoded. Can be any type except
4103
     *                       a resource.
4104
     *                       </p>
4105
     *                       <p>
4106
     *                       All string data must be UTF-8 encoded.
4107
     *                       </p>
4108
     *                       <p>PHP implements a superset of
4109
     *                       JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
4110
     *                       only supports these values when they are nested inside an array or an object.
4111
     *                       </p>
4112
     * @param int   $options [optional] <p>
4113
     *                       Bitmask consisting of <b>JSON_HEX_QUOT</b>,
4114
     *                       <b>JSON_HEX_TAG</b>,
4115
     *                       <b>JSON_HEX_AMP</b>,
4116
     *                       <b>JSON_HEX_APOS</b>,
4117
     *                       <b>JSON_NUMERIC_CHECK</b>,
4118
     *                       <b>JSON_PRETTY_PRINT</b>,
4119
     *                       <b>JSON_UNESCAPED_SLASHES</b>,
4120
     *                       <b>JSON_FORCE_OBJECT</b>,
4121
     *                       <b>JSON_UNESCAPED_UNICODE</b>. The behaviour of these
4122
     *                       constants is described on
4123
     *                       the JSON constants page.
4124
     *                       </p>
4125
     * @param int   $depth   [optional] <p>
4126
     *                       Set the maximum depth. Must be greater than zero.
4127
     *                       </p>
4128
     *
4129
     * @psalm-pure
4130
     *
4131
     * @return false|string
4132
     *                      A JSON encoded <strong>string</strong> on success or<br>
4133
     *                      <strong>FALSE</strong> on failure
4134
     */
4135 5
    public static function json_encode($value, int $options = 0, int $depth = 512)
4136
    {
4137 5
        $value = self::filter($value);
4138
4139 5
        if (self::$SUPPORT['json'] === false) {
4140
            throw new \RuntimeException('ext-json: is not installed');
4141
        }
4142
4143
        /** @noinspection PhpComposerExtensionStubsInspection */
4144 5
        return \json_encode($value, $options, $depth);
4145
    }
4146
4147
    /**
4148
     * Checks whether JSON is available on the server.
4149
     *
4150
     * @psalm-pure
4151
     *
4152
     * @return bool
4153
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
4154
     */
4155
    public static function json_loaded(): bool
4156
    {
4157
        return \function_exists('json_decode');
4158
    }
4159
4160
    /**
4161
     * Makes string's first char lowercase.
4162
     *
4163
     * @param string      $str                           <p>The input string</p>
4164
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
4165
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4166
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
4167
     *                                                   tr</p>
4168
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
4169
     *                                                   -> ß</p>
4170
     *
4171
     * @psalm-pure
4172
     *
4173
     * @return string the resulting string
4174
     */
4175 46
    public static function lcfirst(
4176
        string $str,
4177
        string $encoding = 'UTF-8',
4178
        bool $clean_utf8 = false,
4179
        string $lang = null,
4180
        bool $try_to_keep_the_string_length = false
4181
    ): string {
4182 46
        if ($clean_utf8) {
4183
            $str = self::clean($str);
4184
        }
4185
4186 46
        $use_mb_functions = ($lang === null && !$try_to_keep_the_string_length);
4187
4188 46
        if ($encoding === 'UTF-8') {
4189 43
            $str_part_two = (string) \mb_substr($str, 1);
4190
4191 43
            if ($use_mb_functions) {
4192 43
                $str_part_one = \mb_strtolower(
4193 43
                    (string) \mb_substr($str, 0, 1)
4194
                );
4195
            } else {
4196
                $str_part_one = self::strtolower(
4197
                    (string) \mb_substr($str, 0, 1),
4198
                    $encoding,
4199
                    false,
4200
                    $lang,
4201 43
                    $try_to_keep_the_string_length
4202
                );
4203
            }
4204
        } else {
4205 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4206
4207 3
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
4208
4209 3
            $str_part_one = self::strtolower(
4210 3
                (string) self::substr($str, 0, 1, $encoding),
4211 3
                $encoding,
4212 3
                false,
4213 3
                $lang,
4214 3
                $try_to_keep_the_string_length
4215
            );
4216
        }
4217
4218 46
        return $str_part_one . $str_part_two;
4219
    }
4220
4221
    /**
4222
     * alias for "UTF8::lcfirst()"
4223
     *
4224
     * @param string      $str
4225
     * @param string      $encoding
4226
     * @param bool        $clean_utf8
4227
     * @param string|null $lang
4228
     * @param bool        $try_to_keep_the_string_length
4229
     *
4230
     * @psalm-pure
4231
     *
4232
     * @return string
4233
     *
4234
     * @see        UTF8::lcfirst()
4235
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
4236
     */
4237 2
    public static function lcword(
4238
        string $str,
4239
        string $encoding = 'UTF-8',
4240
        bool $clean_utf8 = false,
4241
        string $lang = null,
4242
        bool $try_to_keep_the_string_length = false
4243
    ): string {
4244 2
        return self::lcfirst(
4245 2
            $str,
4246 2
            $encoding,
4247 2
            $clean_utf8,
4248 2
            $lang,
4249 2
            $try_to_keep_the_string_length
4250
        );
4251
    }
4252
4253
    /**
4254
     * Lowercase for all words in the string.
4255
     *
4256
     * @param string      $str                           <p>The input string.</p>
4257
     * @param string[]    $exceptions                    [optional] <p>Exclusion for some words.</p>
4258
     * @param string      $char_list                     [optional] <p>Additional chars that contains to words and do
4259
     *                                                   not start a new word.</p>
4260
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
4261
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4262
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
4263
     *                                                   tr</p>
4264
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
4265
     *                                                   -> ß</p>
4266
     *
4267
     * @psalm-pure
4268
     *
4269
     * @return string
4270
     */
4271 2
    public static function lcwords(
4272
        string $str,
4273
        array $exceptions = [],
4274
        string $char_list = '',
4275
        string $encoding = 'UTF-8',
4276
        bool $clean_utf8 = false,
4277
        string $lang = null,
4278
        bool $try_to_keep_the_string_length = false
4279
    ): string {
4280 2
        if (!$str) {
4281 2
            return '';
4282
        }
4283
4284 2
        $words = self::str_to_words($str, $char_list);
4285 2
        $use_exceptions = $exceptions !== [];
4286
4287 2
        $words_str = '';
4288 2
        foreach ($words as &$word) {
4289 2
            if (!$word) {
4290 2
                continue;
4291
            }
4292
4293
            if (
4294 2
                !$use_exceptions
4295
                ||
4296 2
                !\in_array($word, $exceptions, true)
4297
            ) {
4298 2
                $words_str .= self::lcfirst($word, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
4299
            } else {
4300 2
                $words_str .= $word;
4301
            }
4302
        }
4303
4304 2
        return $words_str;
4305
    }
4306
4307
    /**
4308
     * alias for "UTF8::lcfirst()"
4309
     *
4310
     * @param string      $str
4311
     * @param string      $encoding
4312
     * @param bool        $clean_utf8
4313
     * @param string|null $lang
4314
     * @param bool        $try_to_keep_the_string_length
4315
     *
4316
     * @psalm-pure
4317
     *
4318
     * @return string
4319
     *
4320
     * @see        UTF8::lcfirst()
4321
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
4322
     */
4323 5
    public static function lowerCaseFirst(
4324
        string $str,
4325
        string $encoding = 'UTF-8',
4326
        bool $clean_utf8 = false,
4327
        string $lang = null,
4328
        bool $try_to_keep_the_string_length = false
4329
    ): string {
4330 5
        return self::lcfirst(
4331 5
            $str,
4332 5
            $encoding,
4333 5
            $clean_utf8,
4334 5
            $lang,
4335 5
            $try_to_keep_the_string_length
4336
        );
4337
    }
4338
4339
    /**
4340
     * Strip whitespace or other characters from the beginning of a UTF-8 string.
4341
     *
4342
     * @param string      $str   <p>The string to be trimmed</p>
4343
     * @param string|null $chars <p>Optional characters to be stripped</p>
4344
     *
4345
     * @psalm-pure
4346
     *
4347
     * @return string the string with unwanted characters stripped from the left
4348
     */
4349 22
    public static function ltrim(string $str = '', string $chars = null): string
4350
    {
4351 22
        if ($str === '') {
4352 3
            return '';
4353
        }
4354
4355 21
        if (self::$SUPPORT['mbstring'] === true) {
4356 21
            if ($chars) {
4357
                /** @noinspection PregQuoteUsageInspection */
4358 10
                $chars = \preg_quote($chars);
4359 10
                $pattern = "^[${chars}]+";
4360
            } else {
4361 14
                $pattern = '^[\\s]+';
4362
            }
4363
4364
            /** @noinspection PhpComposerExtensionStubsInspection */
4365 21
            return (string) \mb_ereg_replace($pattern, '', $str);
4366
        }
4367
4368
        if ($chars) {
4369
            $chars = \preg_quote($chars, '/');
4370
            $pattern = "^[${chars}]+";
4371
        } else {
4372
            $pattern = '^[\\s]+';
4373
        }
4374
4375
        return self::regex_replace($str, $pattern, '', '', '/');
4376
    }
4377
4378
    /**
4379
     * Returns the UTF-8 character with the maximum code point in the given data.
4380
     *
4381
     * @param array<string>|string $arg <p>A UTF-8 encoded string or an array of such strings.</p>
4382
     *
4383
     * @psalm-pure
4384
     *
4385
     * @return string|null the character with the highest code point than others, returns null on failure or empty input
4386
     */
4387 2
    public static function max($arg)
4388
    {
4389 2
        if (\is_array($arg)) {
4390 2
            $arg = \implode('', $arg);
4391
        }
4392
4393 2
        $codepoints = self::codepoints($arg, false);
4394 2
        if ($codepoints === []) {
4395 2
            return null;
4396
        }
4397
4398 2
        $codepoint_max = \max($codepoints);
4399
4400 2
        return self::chr($codepoint_max);
4401
    }
4402
4403
    /**
4404
     * Calculates and returns the maximum number of bytes taken by any
4405
     * UTF-8 encoded character in the given string.
4406
     *
4407
     * @param string $str <p>The original Unicode string.</p>
4408
     *
4409
     * @psalm-pure
4410
     *
4411
     * @return int
4412
     *             <p>Max byte lengths of the given chars.</p>
4413
     */
4414 2
    public static function max_chr_width(string $str): int
4415
    {
4416 2
        $bytes = self::chr_size_list($str);
4417 2
        if ($bytes !== []) {
4418 2
            return (int) \max($bytes);
4419
        }
4420
4421 2
        return 0;
4422
    }
4423
4424
    /**
4425
     * Checks whether mbstring is available on the server.
4426
     *
4427
     * @psalm-pure
4428
     *
4429
     * @return bool
4430
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
4431
     */
4432 26
    public static function mbstring_loaded(): bool
4433
    {
4434 26
        return \extension_loaded('mbstring');
4435
    }
4436
4437
    /**
4438
     * Returns the UTF-8 character with the minimum code point in the given data.
4439
     *
4440
     * @param mixed $arg <strong>A UTF-8 encoded string or an array of such strings.</strong>
4441
     *
4442
     * @psalm-pure
4443
     *
4444
     * @return string|null the character with the lowest code point than others, returns null on failure or empty input
4445
     */
4446 2
    public static function min($arg)
4447
    {
4448 2
        if (\is_array($arg)) {
4449 2
            $arg = \implode('', $arg);
4450
        }
4451
4452 2
        $codepoints = self::codepoints($arg, false);
4453 2
        if ($codepoints === []) {
4454 2
            return null;
4455
        }
4456
4457 2
        $codepoint_min = \min($codepoints);
4458
4459 2
        return self::chr($codepoint_min);
4460
    }
4461
4462
    /**
4463
     * alias for "UTF8::normalize_encoding()"
4464
     *
4465
     * @param mixed $encoding
4466
     * @param mixed $fallback
4467
     *
4468
     * @psalm-pure
4469
     *
4470
     * @return mixed
4471
     *
4472
     * @see        UTF8::normalize_encoding()
4473
     * @deprecated <p>please use "UTF8::normalize_encoding()"</p>
4474
     */
4475 2
    public static function normalizeEncoding($encoding, $fallback = '')
4476
    {
4477 2
        return self::normalize_encoding($encoding, $fallback);
4478
    }
4479
4480
    /**
4481
     * Normalize the encoding-"name" input.
4482
     *
4483
     * @param mixed $encoding <p>e.g.: ISO, UTF8, WINDOWS-1251 etc.</p>
4484
     * @param mixed $fallback <p>e.g.: UTF-8</p>
4485
     *
4486
     * @psalm-pure
4487
     *
4488
     * @return mixed e.g.: ISO-8859-1, UTF-8, WINDOWS-1251 etc.<br>Will return a empty string as fallback (by default)
4489
     */
4490 339
    public static function normalize_encoding($encoding, $fallback = '')
4491
    {
4492
        /**
4493
         * @psalm-suppress ImpureStaticVariable
4494
         *
4495
         * @var array<string,string>
4496
         */
4497 339
        static $STATIC_NORMALIZE_ENCODING_CACHE = [];
4498
4499
        // init
4500 339
        $encoding = (string) $encoding;
4501
4502 339
        if (!$encoding) {
4503 290
            return $fallback;
4504
        }
4505
4506
        if (
4507 53
            $encoding === 'UTF-8'
4508
            ||
4509 53
            $encoding === 'UTF8'
4510
        ) {
4511 29
            return 'UTF-8';
4512
        }
4513
4514
        if (
4515 44
            $encoding === '8BIT'
4516
            ||
4517 44
            $encoding === 'BINARY'
4518
        ) {
4519
            return 'CP850';
4520
        }
4521
4522
        if (
4523 44
            $encoding === 'HTML'
4524
            ||
4525 44
            $encoding === 'HTML-ENTITIES'
4526
        ) {
4527 2
            return 'HTML-ENTITIES';
4528
        }
4529
4530
        if (
4531 44
            $encoding === 'ISO'
4532
            ||
4533 44
            $encoding === 'ISO-8859-1'
4534
        ) {
4535 41
            return 'ISO-8859-1';
4536
        }
4537
4538
        if (
4539 11
            $encoding === '1' // only a fallback, for non "strict_types" usage ...
4540
            ||
4541 11
            $encoding === '0' // only a fallback, for non "strict_types" usage ...
4542
        ) {
4543
            return $fallback;
4544
        }
4545
4546 11
        if (isset($STATIC_NORMALIZE_ENCODING_CACHE[$encoding])) {
4547 8
            return $STATIC_NORMALIZE_ENCODING_CACHE[$encoding];
4548
        }
4549
4550 5
        if (self::$ENCODINGS === null) {
4551 1
            self::$ENCODINGS = self::getData('encodings');
4552
        }
4553
4554 5
        if (\in_array($encoding, self::$ENCODINGS, true)) {
4555 3
            $STATIC_NORMALIZE_ENCODING_CACHE[$encoding] = $encoding;
4556
4557 3
            return $encoding;
4558
        }
4559
4560 4
        $encoding_original = $encoding;
4561 4
        $encoding = \strtoupper($encoding);
4562 4
        $encoding_upper_helper = (string) \preg_replace('/[^a-zA-Z0-9]/u', '', $encoding);
4563
4564
        $equivalences = [
4565 4
            'ISO8859'     => 'ISO-8859-1',
4566
            'ISO88591'    => 'ISO-8859-1',
4567
            'ISO'         => 'ISO-8859-1',
4568
            'LATIN'       => 'ISO-8859-1',
4569
            'LATIN1'      => 'ISO-8859-1', // Western European
4570
            'ISO88592'    => 'ISO-8859-2',
4571
            'LATIN2'      => 'ISO-8859-2', // Central European
4572
            'ISO88593'    => 'ISO-8859-3',
4573
            'LATIN3'      => 'ISO-8859-3', // Southern European
4574
            'ISO88594'    => 'ISO-8859-4',
4575
            'LATIN4'      => 'ISO-8859-4', // Northern European
4576
            'ISO88595'    => 'ISO-8859-5',
4577
            'ISO88596'    => 'ISO-8859-6', // Greek
4578
            'ISO88597'    => 'ISO-8859-7',
4579
            'ISO88598'    => 'ISO-8859-8', // Hebrew
4580
            'ISO88599'    => 'ISO-8859-9',
4581
            'LATIN5'      => 'ISO-8859-9', // Turkish
4582
            'ISO885911'   => 'ISO-8859-11',
4583
            'TIS620'      => 'ISO-8859-11', // Thai
4584
            'ISO885910'   => 'ISO-8859-10',
4585
            'LATIN6'      => 'ISO-8859-10', // Nordic
4586
            'ISO885913'   => 'ISO-8859-13',
4587
            'LATIN7'      => 'ISO-8859-13', // Baltic
4588
            'ISO885914'   => 'ISO-8859-14',
4589
            'LATIN8'      => 'ISO-8859-14', // Celtic
4590
            'ISO885915'   => 'ISO-8859-15',
4591
            'LATIN9'      => 'ISO-8859-15', // Western European (with some extra chars e.g. €)
4592
            'ISO885916'   => 'ISO-8859-16',
4593
            'LATIN10'     => 'ISO-8859-16', // Southeast European
4594
            'CP1250'      => 'WINDOWS-1250',
4595
            'WIN1250'     => 'WINDOWS-1250',
4596
            'WINDOWS1250' => 'WINDOWS-1250',
4597
            'CP1251'      => 'WINDOWS-1251',
4598
            'WIN1251'     => 'WINDOWS-1251',
4599
            'WINDOWS1251' => 'WINDOWS-1251',
4600
            'CP1252'      => 'WINDOWS-1252',
4601
            'WIN1252'     => 'WINDOWS-1252',
4602
            'WINDOWS1252' => 'WINDOWS-1252',
4603
            'CP1253'      => 'WINDOWS-1253',
4604
            'WIN1253'     => 'WINDOWS-1253',
4605
            'WINDOWS1253' => 'WINDOWS-1253',
4606
            'CP1254'      => 'WINDOWS-1254',
4607
            'WIN1254'     => 'WINDOWS-1254',
4608
            'WINDOWS1254' => 'WINDOWS-1254',
4609
            'CP1255'      => 'WINDOWS-1255',
4610
            'WIN1255'     => 'WINDOWS-1255',
4611
            'WINDOWS1255' => 'WINDOWS-1255',
4612
            'CP1256'      => 'WINDOWS-1256',
4613
            'WIN1256'     => 'WINDOWS-1256',
4614
            'WINDOWS1256' => 'WINDOWS-1256',
4615
            'CP1257'      => 'WINDOWS-1257',
4616
            'WIN1257'     => 'WINDOWS-1257',
4617
            'WINDOWS1257' => 'WINDOWS-1257',
4618
            'CP1258'      => 'WINDOWS-1258',
4619
            'WIN1258'     => 'WINDOWS-1258',
4620
            'WINDOWS1258' => 'WINDOWS-1258',
4621
            'UTF16'       => 'UTF-16',
4622
            'UTF32'       => 'UTF-32',
4623
            'UTF8'        => 'UTF-8',
4624
            'UTF'         => 'UTF-8',
4625
            'UTF7'        => 'UTF-7',
4626
            '8BIT'        => 'CP850',
4627
            'BINARY'      => 'CP850',
4628
        ];
4629
4630 4
        if (!empty($equivalences[$encoding_upper_helper])) {
4631 3
            $encoding = $equivalences[$encoding_upper_helper];
4632
        }
4633
4634 4
        $STATIC_NORMALIZE_ENCODING_CACHE[$encoding_original] = $encoding;
4635
4636 4
        return $encoding;
4637
    }
4638
4639
    /**
4640
     * Standardize line ending to unix-like.
4641
     *
4642
     * @param string          $str      <p>The input string.</p>
4643
     * @param string|string[] $replacer <p>The replacer char e.g. "\n" (Linux) or "\r\n" (Windows). You can also use \PHP_EOL
4644
     *                                  here.</p>
4645
     *
4646
     * @psalm-pure
4647
     *
4648
     * @return string
4649
     *                <p>A string with normalized line ending.</p>
4650
     */
4651 5
    public static function normalize_line_ending(string $str, $replacer = "\n"): string
4652
    {
4653 5
        return \str_replace(["\r\n", "\r", "\n"], $replacer, $str);
4654
    }
4655
4656
    /**
4657
     * Normalize some MS Word special characters.
4658
     *
4659
     * @param string $str <p>The string to be normalized.</p>
4660
     *
4661
     * @psalm-pure
4662
     *
4663
     * @return string
4664
     *                <p>A string with normalized characters for commonly used chars in Word documents.</p>
4665
     */
4666 10
    public static function normalize_msword(string $str): string
4667
    {
4668 10
        return ASCII::normalize_msword($str);
4669
    }
4670
4671
    /**
4672
     * Normalize the whitespace.
4673
     *
4674
     * @param string $str                        <p>The string to be normalized.</p>
4675
     * @param bool   $keep_non_breaking_space    [optional] <p>Set to true, to keep non-breaking-spaces.</p>
4676
     * @param bool   $keep_bidi_unicode_controls [optional] <p>Set to true, to keep non-printable (for the web)
4677
     *                                           bidirectional text chars.</p>
4678
     *
4679
     * @psalm-pure
4680
     *
4681
     * @return string
4682
     *                <p>A string with normalized whitespace.</p>
4683
     */
4684 61
    public static function normalize_whitespace(
4685
        string $str,
4686
        bool $keep_non_breaking_space = false,
4687
        bool $keep_bidi_unicode_controls = false
4688
    ): string {
4689 61
        return ASCII::normalize_whitespace(
4690 61
            $str,
4691 61
            $keep_non_breaking_space,
4692 61
            $keep_bidi_unicode_controls
4693
        );
4694
    }
4695
4696
    /**
4697
     * Calculates Unicode code point of the given UTF-8 encoded character.
4698
     *
4699
     * INFO: opposite to UTF8::chr()
4700
     *
4701
     * @param string $chr      <p>The character of which to calculate code point.<p/>
4702
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
4703
     *
4704
     * @psalm-pure
4705
     *
4706
     * @return int
4707
     *             <p>Unicode code point of the given character,<br>
4708
     *             0 on invalid UTF-8 byte sequence</p>
4709
     */
4710 27
    public static function ord($chr, string $encoding = 'UTF-8'): int
4711
    {
4712
        /**
4713
         * @psalm-suppress ImpureStaticVariable
4714
         *
4715
         * @var array<string,int>
4716
         */
4717 27
        static $CHAR_CACHE = [];
4718
4719
        // init
4720 27
        $chr = (string) $chr;
4721
4722 27
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
4723 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4724
        }
4725
4726 27
        $cache_key = $chr . '_' . $encoding;
4727 27
        if (isset($CHAR_CACHE[$cache_key])) {
4728 27
            return $CHAR_CACHE[$cache_key];
4729
        }
4730
4731
        // check again, if it's still not UTF-8
4732 11
        if ($encoding !== 'UTF-8') {
4733 3
            $chr = self::encode($encoding, $chr);
4734
        }
4735
4736 11
        if (self::$ORD === null) {
4737
            self::$ORD = self::getData('ord');
4738
        }
4739
4740 11
        if (isset(self::$ORD[$chr])) {
4741 11
            return $CHAR_CACHE[$cache_key] = self::$ORD[$chr];
4742
        }
4743
4744
        //
4745
        // fallback via "IntlChar"
4746
        //
4747
4748 6
        if (self::$SUPPORT['intlChar'] === true) {
4749
            /** @noinspection PhpComposerExtensionStubsInspection */
4750 5
            $code = \IntlChar::ord($chr);
4751 5
            if ($code) {
4752 5
                return $CHAR_CACHE[$cache_key] = $code;
4753
            }
4754
        }
4755
4756
        //
4757
        // fallback via vanilla php
4758
        //
4759
4760
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
4761 1
        $chr = \unpack('C*', (string) \substr($chr, 0, 4));
4762
        /** @noinspection OffsetOperationsInspection */
4763 1
        $code = $chr ? $chr[1] : 0;
4764
4765
        /** @noinspection OffsetOperationsInspection */
4766 1
        if ($code >= 0xF0 && isset($chr[4])) {
4767
            /** @noinspection UnnecessaryCastingInspection */
4768
            /** @noinspection OffsetOperationsInspection */
4769
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xF0) << 18) + (($chr[2] - 0x80) << 12) + (($chr[3] - 0x80) << 6) + $chr[4] - 0x80);
4770
        }
4771
4772
        /** @noinspection OffsetOperationsInspection */
4773 1
        if ($code >= 0xE0 && isset($chr[3])) {
4774
            /** @noinspection UnnecessaryCastingInspection */
4775
            /** @noinspection OffsetOperationsInspection */
4776 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xE0) << 12) + (($chr[2] - 0x80) << 6) + $chr[3] - 0x80);
4777
        }
4778
4779
        /** @noinspection OffsetOperationsInspection */
4780 1
        if ($code >= 0xC0 && isset($chr[2])) {
4781
            /** @noinspection UnnecessaryCastingInspection */
4782
            /** @noinspection OffsetOperationsInspection */
4783 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xC0) << 6) + $chr[2] - 0x80);
4784
        }
4785
4786
        return $CHAR_CACHE[$cache_key] = $code;
4787
    }
4788
4789
    /**
4790
     * Parses the string into an array (into the the second parameter).
4791
     *
4792
     * WARNING: Unlike "parse_str()", this method does not (re-)place variables in the current scope,
4793
     *          if the second parameter is not set!
4794
     *
4795
     * @see http://php.net/manual/en/function.parse-str.php
4796
     *
4797
     * @param string $str        <p>The input string.</p>
4798
     * @param array  $result     <p>The result will be returned into this reference parameter.</p>
4799
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
4800
     *
4801
     * @psalm-pure
4802
     *
4803
     * @return bool
4804
     *              <p>Will return <strong>false</strong> if php can't parse the string and we haven't any $result.</p>
4805
     */
4806 2
    public static function parse_str(string $str, &$result, bool $clean_utf8 = false): bool
4807
    {
4808 2
        if ($clean_utf8) {
4809 2
            $str = self::clean($str);
4810
        }
4811
4812 2
        if (self::$SUPPORT['mbstring'] === true) {
4813 2
            $return = \mb_parse_str($str, $result);
4814
4815 2
            return $return !== false && $result !== [];
4816
        }
4817
4818
        /**
4819
         * @psalm-suppress ImpureFunctionCall - we use the second parameter, so we don't change variables by magic
4820
         */
4821
        \parse_str($str, $result);
4822
4823
        return $result !== [];
4824
    }
4825
4826
    /**
4827
     * Checks if \u modifier is available that enables Unicode support in PCRE.
4828
     *
4829
     * @psalm-pure
4830
     *
4831
     * @return bool
4832
     *              <p>
4833
     *              <strong>true</strong> if support is available,<br>
4834
     *              <strong>false</strong> otherwise
4835
     *              </p>
4836
     */
4837 102
    public static function pcre_utf8_support(): bool
4838
    {
4839
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
4840 102
        return (bool) @\preg_match('//u', '');
4841
    }
4842
4843
    /**
4844
     * Create an array containing a range of UTF-8 characters.
4845
     *
4846
     * @param mixed     $var1      <p>Numeric or hexadecimal code points, or a UTF-8 character to start from.</p>
4847
     * @param mixed     $var2      <p>Numeric or hexadecimal code points, or a UTF-8 character to end at.</p>
4848
     * @param bool      $use_ctype <p>use ctype to detect numeric and hexadecimal, otherwise we will use a simple
4849
     *                             "is_numeric"</p>
4850
     * @param string    $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
4851
     * @param float|int $step      [optional] <p>
4852
     *                             If a step value is given, it will be used as the
4853
     *                             increment between elements in the sequence. step
4854
     *                             should be given as a positive number. If not specified,
4855
     *                             step will default to 1.
4856
     *                             </p>
4857
     *
4858
     * @psalm-pure
4859
     *
4860
     * @return string[]
4861
     */
4862 2
    public static function range(
4863
        $var1,
4864
        $var2,
4865
        bool $use_ctype = true,
4866
        string $encoding = 'UTF-8',
4867
        $step = 1
4868
    ): array {
4869 2
        if (!$var1 || !$var2) {
4870 2
            return [];
4871
        }
4872
4873 2
        if ($step !== 1) {
4874
            /**
4875
             * @psalm-suppress RedundantConditionGivenDocblockType
4876
             * @psalm-suppress DocblockTypeContradiction
4877
             */
4878 1
            if (!\is_numeric($step)) {
0 ignored issues
show
introduced by
The condition is_numeric($step) is always true.
Loading history...
4879
                throw new \InvalidArgumentException('$step need to be a number, type given: ' . \gettype($step));
4880
            }
4881
4882
            /**
4883
             * @psalm-suppress RedundantConditionGivenDocblockType - false-positive from psalm?
4884
             */
4885 1
            if ($step <= 0) {
4886
                throw new \InvalidArgumentException('$step need to be a positive number, given: ' . $step);
4887
            }
4888
        }
4889
4890 2
        if ($use_ctype && self::$SUPPORT['ctype'] === false) {
4891
            throw new \RuntimeException('ext-ctype: is not installed');
4892
        }
4893
4894 2
        $is_digit = false;
4895 2
        $is_xdigit = false;
4896
4897
        /** @noinspection PhpComposerExtensionStubsInspection */
4898 2
        if ($use_ctype && \ctype_digit((string) $var1) && \ctype_digit((string) $var2)) {
4899 2
            $is_digit = true;
4900 2
            $start = (int) $var1;
4901 2
        } /** @noinspection PhpComposerExtensionStubsInspection */ elseif ($use_ctype && \ctype_xdigit($var1) && \ctype_xdigit($var2)) {
4902
            $is_xdigit = true;
4903
            $start = (int) self::hex_to_int($var1);
4904 2
        } elseif (!$use_ctype && \is_numeric($var1)) {
4905 1
            $start = (int) $var1;
4906
        } else {
4907 2
            $start = self::ord($var1);
4908
        }
4909
4910 2
        if (!$start) {
4911
            return [];
4912
        }
4913
4914 2
        if ($is_digit) {
4915 2
            $end = (int) $var2;
4916 2
        } elseif ($is_xdigit) {
4917
            $end = (int) self::hex_to_int($var2);
4918 2
        } elseif (!$use_ctype && \is_numeric($var2)) {
4919 1
            $end = (int) $var2;
4920
        } else {
4921 2
            $end = self::ord($var2);
4922
        }
4923
4924 2
        if (!$end) {
4925
            return [];
4926
        }
4927
4928 2
        $array = [];
4929 2
        foreach (\range($start, $end, $step) as $i) {
4930 2
            $array[] = (string) self::chr((int) $i, $encoding);
4931
        }
4932
4933 2
        return $array;
4934
    }
4935
4936
    /**
4937
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
4938
     *
4939
     * e.g:
4940
     * 'test+test'                     => 'test+test'
4941
     * 'D&#252;sseldorf'               => 'Düsseldorf'
4942
     * 'D%FCsseldorf'                  => 'Düsseldorf'
4943
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
4944
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
4945
     * 'Düsseldorf'                   => 'Düsseldorf'
4946
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
4947
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
4948
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
4949
     *
4950
     * @param string $str          <p>The input string.</p>
4951
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
4952
     *
4953
     * @psalm-pure
4954
     *
4955
     * @return string
4956
     *                <p>The decoded URL, as a string.</p>
4957
     */
4958 7
    public static function rawurldecode(string $str, bool $multi_decode = true): string
4959
    {
4960 7
        if ($str === '') {
4961 4
            return '';
4962
        }
4963
4964
        if (
4965 7
            \strpos($str, '&') === false
4966
            &&
4967 7
            \strpos($str, '%') === false
4968
            &&
4969 7
            \strpos($str, '+') === false
4970
            &&
4971 7
            \strpos($str, '\u') === false
4972
        ) {
4973 4
            return self::fix_simple_utf8($str);
4974
        }
4975
4976 7
        $str = self::urldecode_unicode_helper($str);
4977
4978 7
        if ($multi_decode) {
4979
            do {
4980 6
                $str_compare = $str;
4981
4982
                /**
4983
                 * @psalm-suppress PossiblyInvalidArgument
4984
                 */
4985 6
                $str = self::fix_simple_utf8(
4986 6
                    \rawurldecode(
4987 6
                        self::html_entity_decode(
4988 6
                            self::to_utf8($str),
4989 6
                            \ENT_QUOTES | \ENT_HTML5
4990
                        )
4991
                    )
4992
                );
4993 6
            } while ($str_compare !== $str);
4994
        } else {
4995
            /**
4996
             * @psalm-suppress PossiblyInvalidArgument
4997
             */
4998 1
            $str = self::fix_simple_utf8(
4999 1
                \rawurldecode(
5000 1
                    self::html_entity_decode(
5001 1
                        self::to_utf8($str),
5002 1
                        \ENT_QUOTES | \ENT_HTML5
5003
                    )
5004
                )
5005
            );
5006
        }
5007
5008 7
        return $str;
5009
    }
5010
5011
    /**
5012
     * Replaces all occurrences of $pattern in $str by $replacement.
5013
     *
5014
     * @param string $str         <p>The input string.</p>
5015
     * @param string $pattern     <p>The regular expression pattern.</p>
5016
     * @param string $replacement <p>The string to replace with.</p>
5017
     * @param string $options     [optional] <p>Matching conditions to be used.</p>
5018
     * @param string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
5019
     *
5020
     * @psalm-pure
5021
     *
5022
     * @return string
5023
     */
5024 18
    public static function regex_replace(
5025
        string $str,
5026
        string $pattern,
5027
        string $replacement,
5028
        string $options = '',
5029
        string $delimiter = '/'
5030
    ): string {
5031 18
        if ($options === 'msr') {
5032 9
            $options = 'ms';
5033
        }
5034
5035
        // fallback
5036 18
        if (!$delimiter) {
5037
            $delimiter = '/';
5038
        }
5039
5040 18
        return (string) \preg_replace(
5041 18
            $delimiter . $pattern . $delimiter . 'u' . $options,
5042 18
            $replacement,
5043 18
            $str
5044
        );
5045
    }
5046
5047
    /**
5048
     * alias for "UTF8::remove_bom()"
5049
     *
5050
     * @param string $str
5051
     *
5052
     * @psalm-pure
5053
     *
5054
     * @return string
5055
     *
5056
     * @see        UTF8::remove_bom()
5057
     * @deprecated <p>please use "UTF8::remove_bom()"</p>
5058
     */
5059 1
    public static function removeBOM(string $str): string
5060
    {
5061 1
        return self::remove_bom($str);
5062
    }
5063
5064
    /**
5065
     * Remove the BOM from UTF-8 / UTF-16 / UTF-32 strings.
5066
     *
5067
     * @param string $str <p>The input string.</p>
5068
     *
5069
     * @psalm-pure
5070
     *
5071
     * @return string
5072
     *                <p>A string without UTF-BOM.</p>
5073
     */
5074 55
    public static function remove_bom(string $str): string
5075
    {
5076 55
        if ($str === '') {
5077 9
            return '';
5078
        }
5079
5080 55
        $str_length = \strlen($str);
5081 55
        foreach (self::$BOM as $bom_string => $bom_byte_length) {
5082 55
            if (\strpos($str, $bom_string, 0) === 0) {
5083
                /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
5084 11
                $str_tmp = \substr($str, $bom_byte_length, $str_length);
5085 11
                if ($str_tmp === false) {
5086
                    return '';
5087
                }
5088
5089 11
                $str_length -= (int) $bom_byte_length;
5090
5091 55
                $str = (string) $str_tmp;
5092
            }
5093
        }
5094
5095 55
        return $str;
5096
    }
5097
5098
    /**
5099
     * Removes duplicate occurrences of a string in another string.
5100
     *
5101
     * @param string          $str  <p>The base string.</p>
5102
     * @param string|string[] $what <p>String to search for in the base string.</p>
5103
     *
5104
     * @psalm-pure
5105
     *
5106
     * @return string
5107
     *                <p>A string with removed duplicates.</p>
5108
     */
5109 2
    public static function remove_duplicates(string $str, $what = ' '): string
5110
    {
5111 2
        if (\is_string($what)) {
5112 2
            $what = [$what];
5113
        }
5114
5115
        /**
5116
         * @psalm-suppress RedundantConditionGivenDocblockType
5117
         */
5118 2
        if (\is_array($what)) {
0 ignored issues
show
introduced by
The condition is_array($what) is always true.
Loading history...
5119 2
            foreach ($what as $item) {
5120 2
                $str = (string) \preg_replace('/(' . \preg_quote($item, '/') . ')+/u', $item, $str);
5121
            }
5122
        }
5123
5124 2
        return $str;
5125
    }
5126
5127
    /**
5128
     * Remove html via "strip_tags()" from the string.
5129
     *
5130
     * @param string $str            <p>The input string.</p>
5131
     * @param string $allowable_tags [optional] <p>You can use the optional second parameter to specify tags which
5132
     *                               should not be stripped. Default: null
5133
     *                               </p>
5134
     *
5135
     * @psalm-pure
5136
     *
5137
     * @return string
5138
     *                <p>A string with without html tags.</p>
5139
     */
5140 6
    public static function remove_html(string $str, string $allowable_tags = ''): string
5141
    {
5142 6
        return \strip_tags($str, $allowable_tags);
5143
    }
5144
5145
    /**
5146
     * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
5147
     *
5148
     * @param string $str         <p>The input string.</p>
5149
     * @param string $replacement [optional] <p>Default is a empty string.</p>
5150
     *
5151
     * @psalm-pure
5152
     *
5153
     * @return string
5154
     *                <p>A string without breaks.</p>
5155
     */
5156 6
    public static function remove_html_breaks(string $str, string $replacement = ''): string
5157
    {
5158 6
        return (string) \preg_replace("#/\r\n|\r|\n|<br.*/?>#isU", $replacement, $str);
5159
    }
5160
5161
    /**
5162
     * Remove invisible characters from a string.
5163
     *
5164
     * e.g.: This prevents sandwiching null characters between ascii characters, like Java\0script.
5165
     *
5166
     * copy&past from https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Common.php
5167
     *
5168
     * @param string $str         <p>The input string.</p>
5169
     * @param bool   $url_encoded [optional] <p>
5170
     *                            Try to remove url encoded control character.
5171
     *                            WARNING: maybe contains false-positives e.g. aa%0Baa -> aaaa.
5172
     *                            <br>
5173
     *                            Default: false
5174
     *                            </p>
5175
     * @param string $replacement [optional] <p>The replacement character.</p>
5176
     *
5177
     * @psalm-pure
5178
     *
5179
     * @return string
5180
     *                <p>A string without invisible chars.</p>
5181
     */
5182 90
    public static function remove_invisible_characters(
5183
        string $str,
5184
        bool $url_encoded = false,
5185
        string $replacement = ''
5186
    ): string {
5187 90
        return ASCII::remove_invisible_characters(
5188 90
            $str,
5189 90
            $url_encoded,
5190 90
            $replacement
5191
        );
5192
    }
5193
5194
    /**
5195
     * Returns a new string with the prefix $substring removed, if present.
5196
     *
5197
     * @param string $str       <p>The input string.</p>
5198
     * @param string $substring <p>The prefix to remove.</p>
5199
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5200
     *
5201
     * @psalm-pure
5202
     *
5203
     * @return string
5204
     *                <p>A string without the prefix $substring.</p>
5205
     */
5206 12
    public static function remove_left(
5207
        string $str,
5208
        string $substring,
5209
        string $encoding = 'UTF-8'
5210
    ): string {
5211 12
        if ($substring && \strpos($str, $substring) === 0) {
5212 6
            if ($encoding === 'UTF-8') {
5213 4
                return (string) \mb_substr(
5214 4
                    $str,
5215 4
                    (int) \mb_strlen($substring)
5216
                );
5217
            }
5218
5219 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5220
5221 2
            return (string) self::substr(
5222 2
                $str,
5223 2
                (int) self::strlen($substring, $encoding),
5224 2
                null,
5225 2
                $encoding
5226
            );
5227
        }
5228
5229 6
        return $str;
5230
    }
5231
5232
    /**
5233
     * Returns a new string with the suffix $substring removed, if present.
5234
     *
5235
     * @param string $str
5236
     * @param string $substring <p>The suffix to remove.</p>
5237
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5238
     *
5239
     * @psalm-pure
5240
     *
5241
     * @return string
5242
     *                <p>A string having a $str without the suffix $substring.</p>
5243
     */
5244 12
    public static function remove_right(
5245
        string $str,
5246
        string $substring,
5247
        string $encoding = 'UTF-8'
5248
    ): string {
5249 12
        if ($substring && \substr($str, -\strlen($substring)) === $substring) {
5250 6
            if ($encoding === 'UTF-8') {
5251 4
                return (string) \mb_substr(
5252 4
                    $str,
5253 4
                    0,
5254 4
                    (int) \mb_strlen($str) - (int) \mb_strlen($substring)
5255
                );
5256
            }
5257
5258 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5259
5260 2
            return (string) self::substr(
5261 2
                $str,
5262 2
                0,
5263 2
                (int) self::strlen($str, $encoding) - (int) self::strlen($substring, $encoding),
5264 2
                $encoding
5265
            );
5266
        }
5267
5268 6
        return $str;
5269
    }
5270
5271
    /**
5272
     * Replaces all occurrences of $search in $str by $replacement.
5273
     *
5274
     * @param string $str            <p>The input string.</p>
5275
     * @param string $search         <p>The needle to search for.</p>
5276
     * @param string $replacement    <p>The string to replace with.</p>
5277
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5278
     *
5279
     * @psalm-pure
5280
     *
5281
     * @return string
5282
     *                <p>A string with replaced parts.</p>
5283
     */
5284 29
    public static function replace(
5285
        string $str,
5286
        string $search,
5287
        string $replacement,
5288
        bool $case_sensitive = true
5289
    ): string {
5290 29
        if ($case_sensitive) {
5291 22
            return \str_replace($search, $replacement, $str);
5292
        }
5293
5294 7
        return self::str_ireplace($search, $replacement, $str);
5295
    }
5296
5297
    /**
5298
     * Replaces all occurrences of $search in $str by $replacement.
5299
     *
5300
     * @param string       $str            <p>The input string.</p>
5301
     * @param array        $search         <p>The elements to search for.</p>
5302
     * @param array|string $replacement    <p>The string to replace with.</p>
5303
     * @param bool         $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5304
     *
5305
     * @psalm-pure
5306
     *
5307
     * @return string
5308
     *                <p>A string with replaced parts.</p>
5309
     */
5310 30
    public static function replace_all(
5311
        string $str,
5312
        array $search,
5313
        $replacement,
5314
        bool $case_sensitive = true
5315
    ): string {
5316 30
        if ($case_sensitive) {
5317 23
            return \str_replace($search, $replacement, $str);
5318
        }
5319
5320 7
        return self::str_ireplace($search, $replacement, $str);
5321
    }
5322
5323
    /**
5324
     * Replace the diamond question mark (�) and invalid-UTF8 chars with the replacement.
5325
     *
5326
     * @param string $str                        <p>The input string</p>
5327
     * @param string $replacement_char           <p>The replacement character.</p>
5328
     * @param bool   $process_invalid_utf8_chars <p>Convert invalid UTF-8 chars </p>
5329
     *
5330
     * @psalm-pure
5331
     *
5332
     * @return string
5333
     *                <p>A string without diamond question marks (�).</p>
5334
     */
5335 35
    public static function replace_diamond_question_mark(
5336
        string $str,
5337
        string $replacement_char = '',
5338
        bool $process_invalid_utf8_chars = true
5339
    ): string {
5340 35
        if ($str === '') {
5341 9
            return '';
5342
        }
5343
5344 35
        if ($process_invalid_utf8_chars) {
5345 35
            $replacement_char_helper = $replacement_char;
5346 35
            if ($replacement_char === '') {
5347 35
                $replacement_char_helper = 'none';
5348
            }
5349
5350 35
            if (self::$SUPPORT['mbstring'] === false) {
5351
                // if there is no native support for "mbstring",
5352
                // then we need to clean the string before ...
5353
                $str = self::clean($str);
5354
            }
5355
5356
            /**
5357
             * @psalm-suppress ImpureFunctionCall - we will reset the value in the next step
5358
             */
5359 35
            $save = \mb_substitute_character();
5360
            /** @noinspection PhpUsageOfSilenceOperatorInspection - ignore "Unknown character" warnings, it's working anyway */
5361 35
            @\mb_substitute_character($replacement_char_helper);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mb_substitute_character(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

5361
            /** @scrutinizer ignore-unhandled */ @\mb_substitute_character($replacement_char_helper);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
5362
            // the polyfill maybe return false, so cast to string
5363 35
            $str = (string) \mb_convert_encoding($str, 'UTF-8', 'UTF-8');
5364 35
            \mb_substitute_character($save);
5365
        }
5366
5367 35
        return \str_replace(
5368
            [
5369 35
                "\xEF\xBF\xBD",
5370
                '�',
5371
            ],
5372
            [
5373 35
                $replacement_char,
5374 35
                $replacement_char,
5375
            ],
5376 35
            $str
5377
        );
5378
    }
5379
5380
    /**
5381
     * Strip whitespace or other characters from the end of a UTF-8 string.
5382
     *
5383
     * @param string      $str   <p>The string to be trimmed.</p>
5384
     * @param string|null $chars <p>Optional characters to be stripped.</p>
5385
     *
5386
     * @psalm-pure
5387
     *
5388
     * @return string
5389
     *                <p>A string with unwanted characters stripped from the right.</p>
5390
     */
5391 20
    public static function rtrim(string $str = '', string $chars = null): string
5392
    {
5393 20
        if ($str === '') {
5394 3
            return '';
5395
        }
5396
5397 19
        if (self::$SUPPORT['mbstring'] === true) {
5398 19
            if ($chars) {
5399
                /** @noinspection PregQuoteUsageInspection */
5400 8
                $chars = \preg_quote($chars);
5401 8
                $pattern = "[${chars}]+$";
5402
            } else {
5403 14
                $pattern = '[\\s]+$';
5404
            }
5405
5406
            /** @noinspection PhpComposerExtensionStubsInspection */
5407 19
            return (string) \mb_ereg_replace($pattern, '', $str);
5408
        }
5409
5410
        if ($chars) {
5411
            $chars = \preg_quote($chars, '/');
5412
            $pattern = "[${chars}]+$";
5413
        } else {
5414
            $pattern = '[\\s]+$';
5415
        }
5416
5417
        return self::regex_replace($str, $pattern, '', '', '/');
5418
    }
5419
5420
    /**
5421
     * WARNING: Print native UTF-8 support (libs) by default, e.g. for debugging.
5422
     *
5423
     * @param bool $useEcho
5424
     *
5425
     * @psalm-pure
5426
     *
5427
     * @return string|void
5428
     */
5429 2
    public static function showSupport(bool $useEcho = true)
5430
    {
5431
        // init
5432 2
        $html = '';
5433
5434 2
        $html .= '<pre>';
5435
        /** @noinspection AlterInForeachInspection */
5436 2
        foreach (self::$SUPPORT as $key => &$value) {
5437 2
            $html .= $key . ' - ' . \print_r($value, true) . "\n<br>";
5438
        }
5439 2
        $html .= '</pre>';
5440
5441 2
        if ($useEcho) {
5442 1
            echo $html;
5443
        }
5444
5445 2
        return $html;
5446
    }
5447
5448
    /**
5449
     * Converts a UTF-8 character to HTML Numbered Entity like "&#123;".
5450
     *
5451
     * @param string $char             <p>The Unicode character to be encoded as numbered entity.</p>
5452
     * @param bool   $keep_ascii_chars <p>Set to <strong>true</strong> to keep ASCII chars.</>
5453
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
5454
     *
5455
     * @psalm-pure
5456
     *
5457
     * @return string
5458
     *                <p>The HTML numbered entity for the given character.</p>
5459
     */
5460 2
    public static function single_chr_html_encode(
5461
        string $char,
5462
        bool $keep_ascii_chars = false,
5463
        string $encoding = 'UTF-8'
5464
    ): string {
5465 2
        if ($char === '') {
5466 2
            return '';
5467
        }
5468
5469
        if (
5470 2
            $keep_ascii_chars
5471
            &&
5472 2
            ASCII::is_ascii($char)
5473
        ) {
5474 2
            return $char;
5475
        }
5476
5477 2
        return '&#' . self::ord($char, $encoding) . ';';
5478
    }
5479
5480
    /**
5481
     * @param string $str
5482
     * @param int    $tab_length
5483
     *
5484
     * @psalm-pure
5485
     *
5486
     * @return string
5487
     */
5488 5
    public static function spaces_to_tabs(string $str, int $tab_length = 4): string
5489
    {
5490 5
        if ($tab_length === 4) {
5491 3
            $tab = '    ';
5492 2
        } elseif ($tab_length === 2) {
5493 1
            $tab = '  ';
5494
        } else {
5495 1
            $tab = \str_repeat(' ', $tab_length);
5496
        }
5497
5498 5
        return \str_replace($tab, "\t", $str);
5499
    }
5500
5501
    /**
5502
     * alias for "UTF8::str_split()"
5503
     *
5504
     * @param int|string $str
5505
     * @param int        $length
5506
     * @param bool       $clean_utf8
5507
     *
5508
     * @psalm-pure
5509
     *
5510
     * @return string[]
5511
     *
5512
     * @see        UTF8::str_split()
5513
     * @deprecated <p>please use "UTF8::str_split()"</p>
5514
     */
5515 9
    public static function split(
5516
        $str,
5517
        int $length = 1,
5518
        bool $clean_utf8 = false
5519
    ): array {
5520
        /** @var string[] */
5521 9
        return self::str_split($str, $length, $clean_utf8);
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::str_split($str, $length, $clean_utf8) returns an array which contains values of type array|string[] which are incompatible with the documented value type string.
Loading history...
5522
    }
5523
5524
    /**
5525
     * alias for "UTF8::str_starts_with()"
5526
     *
5527
     * @param string $haystack
5528
     * @param string $needle
5529
     *
5530
     * @psalm-pure
5531
     *
5532
     * @return bool
5533
     *
5534
     * @see        UTF8::str_starts_with()
5535
     * @deprecated <p>please use "UTF8::str_starts_with()"</p>
5536
     */
5537 1
    public static function str_begins(string $haystack, string $needle): bool
5538
    {
5539 1
        return self::str_starts_with($haystack, $needle);
5540
    }
5541
5542
    /**
5543
     * Returns a camelCase version of the string. Trims surrounding spaces,
5544
     * capitalizes letters following digits, spaces, dashes and underscores,
5545
     * and removes spaces, dashes, as well as underscores.
5546
     *
5547
     * @param string      $str                           <p>The input string.</p>
5548
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
5549
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5550
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5551
     *                                                   tr</p>
5552
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
5553
     *                                                   -> ß</p>
5554
     *
5555
     * @psalm-pure
5556
     *
5557
     * @return string
5558
     */
5559 32
    public static function str_camelize(
5560
        string $str,
5561
        string $encoding = 'UTF-8',
5562
        bool $clean_utf8 = false,
5563
        string $lang = null,
5564
        bool $try_to_keep_the_string_length = false
5565
    ): string {
5566 32
        if ($clean_utf8) {
5567
            $str = self::clean($str);
5568
        }
5569
5570 32
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
5571 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5572
        }
5573
5574 32
        $str = self::lcfirst(
5575 32
            \trim($str),
5576 32
            $encoding,
5577 32
            false,
5578 32
            $lang,
5579 32
            $try_to_keep_the_string_length
5580
        );
5581 32
        $str = (string) \preg_replace('/^[-_]+/', '', $str);
5582
5583 32
        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;
5584
5585 32
        $str = (string) \preg_replace_callback(
5586 32
            '/[-_\\s]+(.)?/u',
5587
            /**
5588
             * @param array $match
5589
             *
5590
             * @psalm-pure
5591
             *
5592
             * @return string
5593
             */
5594
            static function (array $match) use ($use_mb_functions, $encoding, $lang, $try_to_keep_the_string_length): string {
5595 27
                if (isset($match[1])) {
5596 27
                    if ($use_mb_functions) {
5597 27
                        if ($encoding === 'UTF-8') {
5598 27
                            return \mb_strtoupper($match[1]);
5599
                        }
5600
5601
                        return \mb_strtoupper($match[1], $encoding);
5602
                    }
5603
5604
                    return self::strtoupper($match[1], $encoding, false, $lang, $try_to_keep_the_string_length);
5605
                }
5606
5607 1
                return '';
5608 32
            },
5609 32
            $str
5610
        );
5611
5612 32
        return (string) \preg_replace_callback(
5613 32
            '/[\\p{N}]+(.)?/u',
5614
            /**
5615
             * @param array $match
5616
             *
5617
             * @psalm-pure
5618
             *
5619
             * @return string
5620
             */
5621
            static function (array $match) use ($use_mb_functions, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length): string {
5622 6
                if ($use_mb_functions) {
5623 6
                    if ($encoding === 'UTF-8') {
5624 6
                        return \mb_strtoupper($match[0]);
5625
                    }
5626
5627
                    return \mb_strtoupper($match[0], $encoding);
5628
                }
5629
5630
                return self::strtoupper($match[0], $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5631 32
            },
5632 32
            $str
5633
        );
5634
    }
5635
5636
    /**
5637
     * Returns the string with the first letter of each word capitalized,
5638
     * except for when the word is a name which shouldn't be capitalized.
5639
     *
5640
     * @param string $str
5641
     *
5642
     * @psalm-pure
5643
     *
5644
     * @return string
5645
     *                <p>A string with $str capitalized.</p>
5646
     */
5647 1
    public static function str_capitalize_name(string $str): string
5648
    {
5649 1
        return self::str_capitalize_name_helper(
5650 1
            self::str_capitalize_name_helper(
5651 1
                self::collapse_whitespace($str),
5652 1
                ' '
5653
            ),
5654 1
            '-'
5655
        );
5656
    }
5657
5658
    /**
5659
     * Returns true if the string contains $needle, false otherwise. By default
5660
     * the comparison is case-sensitive, but can be made insensitive by setting
5661
     * $case_sensitive to false.
5662
     *
5663
     * @param string $haystack       <p>The input string.</p>
5664
     * @param string $needle         <p>Substring to look for.</p>
5665
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5666
     *
5667
     * @psalm-pure
5668
     *
5669
     * @return bool whether or not $haystack contains $needle
5670
     */
5671 21
    public static function str_contains(
5672
        string $haystack,
5673
        string $needle,
5674
        bool $case_sensitive = true
5675
    ): bool {
5676 21
        if ($case_sensitive) {
5677 11
            return \strpos($haystack, $needle) !== false;
5678
        }
5679
5680 10
        return \mb_stripos($haystack, $needle) !== false;
5681
    }
5682
5683
    /**
5684
     * Returns true if the string contains all $needles, false otherwise. By
5685
     * default the comparison is case-sensitive, but can be made insensitive by
5686
     * setting $case_sensitive to false.
5687
     *
5688
     * @param string $haystack       <p>The input string.</p>
5689
     * @param array  $needles        <p>SubStrings to look for.</p>
5690
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5691
     *
5692
     * @psalm-pure
5693
     *
5694
     * @return bool whether or not $haystack contains $needle
5695
     */
5696 45
    public static function str_contains_all(
5697
        string $haystack,
5698
        array $needles,
5699
        bool $case_sensitive = true
5700
    ): bool {
5701 45
        if ($haystack === '' || $needles === []) {
5702 1
            return false;
5703
        }
5704
5705
        /** @noinspection LoopWhichDoesNotLoopInspection */
5706 44
        foreach ($needles as &$needle) {
5707 44
            if ($case_sensitive) {
5708
                /** @noinspection NestedPositiveIfStatementsInspection */
5709 24
                if (!$needle || \strpos($haystack, $needle) === false) {
5710 12
                    return false;
5711
                }
5712
            }
5713
5714 33
            if (!$needle || \mb_stripos($haystack, $needle) === false) {
5715 33
                return false;
5716
            }
5717
        }
5718
5719 24
        return true;
5720
    }
5721
5722
    /**
5723
     * Returns true if the string contains any $needles, false otherwise. By
5724
     * default the comparison is case-sensitive, but can be made insensitive by
5725
     * setting $case_sensitive to false.
5726
     *
5727
     * @param string $haystack       <p>The input string.</p>
5728
     * @param array  $needles        <p>SubStrings to look for.</p>
5729
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5730
     *
5731
     * @psalm-pure
5732
     *
5733
     * @return bool
5734
     *              Whether or not $str contains $needle
5735
     */
5736 46
    public static function str_contains_any(
5737
        string $haystack,
5738
        array $needles,
5739
        bool $case_sensitive = true
5740
    ): bool {
5741 46
        if ($haystack === '' || $needles === []) {
5742 1
            return false;
5743
        }
5744
5745
        /** @noinspection LoopWhichDoesNotLoopInspection */
5746 45
        foreach ($needles as &$needle) {
5747 45
            if (!$needle) {
5748
                continue;
5749
            }
5750
5751 45
            if ($case_sensitive) {
5752 25
                if (\strpos($haystack, $needle) !== false) {
5753 14
                    return true;
5754
                }
5755
5756 13
                continue;
5757
            }
5758
5759 20
            if (\mb_stripos($haystack, $needle) !== false) {
5760 20
                return true;
5761
            }
5762
        }
5763
5764 19
        return false;
5765
    }
5766
5767
    /**
5768
     * Returns a lowercase and trimmed string separated by dashes. Dashes are
5769
     * inserted before uppercase characters (with the exception of the first
5770
     * character of the string), and in place of spaces as well as underscores.
5771
     *
5772
     * @param string $str      <p>The input string.</p>
5773
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5774
     *
5775
     * @psalm-pure
5776
     *
5777
     * @return string
5778
     */
5779 19
    public static function str_dasherize(string $str, string $encoding = 'UTF-8'): string
5780
    {
5781 19
        return self::str_delimit($str, '-', $encoding);
5782
    }
5783
5784
    /**
5785
     * Returns a lowercase and trimmed string separated by the given delimiter.
5786
     * Delimiters are inserted before uppercase characters (with the exception
5787
     * of the first character of the string), and in place of spaces, dashes,
5788
     * and underscores. Alpha delimiters are not converted to lowercase.
5789
     *
5790
     * @param string      $str                           <p>The input string.</p>
5791
     * @param string      $delimiter                     <p>Sequence used to separate parts of the string.</p>
5792
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
5793
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5794
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5795
     *                                                   tr</p>
5796
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
5797
     *                                                   ß</p>
5798
     *
5799
     * @psalm-pure
5800
     *
5801
     * @return string
5802
     */
5803 49
    public static function str_delimit(
5804
        string $str,
5805
        string $delimiter,
5806
        string $encoding = 'UTF-8',
5807
        bool $clean_utf8 = false,
5808
        string $lang = null,
5809
        bool $try_to_keep_the_string_length = false
5810
    ): string {
5811 49
        if (self::$SUPPORT['mbstring'] === true) {
5812
            /** @noinspection PhpComposerExtensionStubsInspection */
5813 49
            $str = (string) \mb_ereg_replace('\\B(\\p{Lu})', '-\1', \trim($str));
5814
5815 49
            $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;
5816 49
            if ($use_mb_functions && $encoding === 'UTF-8') {
5817 22
                $str = \mb_strtolower($str);
5818
            } else {
5819 27
                $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5820
            }
5821
5822
            /** @noinspection PhpComposerExtensionStubsInspection */
5823 49
            return (string) \mb_ereg_replace('[\\-_\\s]+', $delimiter, $str);
5824
        }
5825
5826
        $str = (string) \preg_replace('/\\B(\\p{Lu})/u', '-\1', \trim($str));
5827
5828
        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;
5829
        if ($use_mb_functions && $encoding === 'UTF-8') {
5830
            $str = \mb_strtolower($str);
5831
        } else {
5832
            $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5833
        }
5834
5835
        return (string) \preg_replace('/[\\-_\\s]+/u', $delimiter, $str);
5836
    }
5837
5838
    /**
5839
     * Optimized "mb_detect_encoding()"-function -> with support for UTF-16 and UTF-32.
5840
     *
5841
     * @param string $str <p>The input string.</p>
5842
     *
5843
     * @psalm-pure
5844
     *
5845
     * @return false|string
5846
     *                      The detected string-encoding e.g. UTF-8 or UTF-16BE,<br>
5847
     *                      otherwise it will return false e.g. for BINARY or not detected encoding.
5848
     */
5849 30
    public static function str_detect_encoding($str)
5850
    {
5851
        // init
5852 30
        $str = (string) $str;
5853
5854
        //
5855
        // 1.) check binary strings (010001001...) like UTF-16 / UTF-32 / PDF / Images / ...
5856
        //
5857
5858 30
        if (self::is_binary($str, true)) {
5859 11
            $is_utf32 = self::is_utf32($str, false);
5860 11
            if ($is_utf32 === 1) {
5861
                return 'UTF-32LE';
5862
            }
5863 11
            if ($is_utf32 === 2) {
5864 1
                return 'UTF-32BE';
5865
            }
5866
5867 11
            $is_utf16 = self::is_utf16($str, false);
5868 11
            if ($is_utf16 === 1) {
5869 3
                return 'UTF-16LE';
5870
            }
5871 11
            if ($is_utf16 === 2) {
5872 2
                return 'UTF-16BE';
5873
            }
5874
5875
            // is binary but not "UTF-16" or "UTF-32"
5876 9
            return false;
5877
        }
5878
5879
        //
5880
        // 2.) simple check for ASCII chars
5881
        //
5882
5883 26
        if (ASCII::is_ascii($str)) {
5884 10
            return 'ASCII';
5885
        }
5886
5887
        //
5888
        // 3.) simple check for UTF-8 chars
5889
        //
5890
5891 26
        if (self::is_utf8_string($str)) {
5892 19
            return 'UTF-8';
5893
        }
5894
5895
        //
5896
        // 4.) check via "mb_detect_encoding()"
5897
        //
5898
        // INFO: UTF-16, UTF-32, UCS2 and UCS4, encoding detection will fail always with "mb_detect_encoding()"
5899
5900
        $encoding_detecting_order = [
5901 15
            'ISO-8859-1',
5902
            'ISO-8859-2',
5903
            'ISO-8859-3',
5904
            'ISO-8859-4',
5905
            'ISO-8859-5',
5906
            'ISO-8859-6',
5907
            'ISO-8859-7',
5908
            'ISO-8859-8',
5909
            'ISO-8859-9',
5910
            'ISO-8859-10',
5911
            'ISO-8859-13',
5912
            'ISO-8859-14',
5913
            'ISO-8859-15',
5914
            'ISO-8859-16',
5915
            'WINDOWS-1251',
5916
            'WINDOWS-1252',
5917
            'WINDOWS-1254',
5918
            'CP932',
5919
            'CP936',
5920
            'CP950',
5921
            'CP866',
5922
            'CP850',
5923
            'CP51932',
5924
            'CP50220',
5925
            'CP50221',
5926
            'CP50222',
5927
            'ISO-2022-JP',
5928
            'ISO-2022-KR',
5929
            'JIS',
5930
            'JIS-ms',
5931
            'EUC-CN',
5932
            'EUC-JP',
5933
        ];
5934
5935 15
        if (self::$SUPPORT['mbstring'] === true) {
5936
            // info: do not use the symfony polyfill here
5937 15
            $encoding = \mb_detect_encoding($str, $encoding_detecting_order, true);
5938 15
            if ($encoding) {
5939 15
                return $encoding;
5940
            }
5941
        }
5942
5943
        //
5944
        // 5.) check via "iconv()"
5945
        //
5946
5947
        if (self::$ENCODINGS === null) {
5948
            self::$ENCODINGS = self::getData('encodings');
5949
        }
5950
5951
        foreach (self::$ENCODINGS as $encoding_tmp) {
5952
            // INFO: //IGNORE but still throw notice
5953
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
5954
            if ((string) @\iconv($encoding_tmp, $encoding_tmp . '//IGNORE', $str) === $str) {
5955
                return $encoding_tmp;
5956
            }
5957
        }
5958
5959
        return false;
5960
    }
5961
5962
    /**
5963
     * alias for "UTF8::str_ends_with()"
5964
     *
5965
     * @param string $haystack
5966
     * @param string $needle
5967
     *
5968
     * @psalm-pure
5969
     *
5970
     * @return bool
5971
     *
5972
     * @see        UTF8::str_ends_with()
5973
     * @deprecated <p>please use "UTF8::str_ends_with()"</p>
5974
     */
5975 1
    public static function str_ends(string $haystack, string $needle): bool
5976
    {
5977 1
        return self::str_ends_with($haystack, $needle);
5978
    }
5979
5980
    /**
5981
     * Check if the string ends with the given substring.
5982
     *
5983
     * @param string $haystack <p>The string to search in.</p>
5984
     * @param string $needle   <p>The substring to search for.</p>
5985
     *
5986
     * @psalm-pure
5987
     *
5988
     * @return bool
5989
     */
5990 9
    public static function str_ends_with(string $haystack, string $needle): bool
5991
    {
5992 9
        if ($needle === '') {
5993 2
            return true;
5994
        }
5995
5996 9
        if ($haystack === '') {
5997
            return false;
5998
        }
5999
6000 9
        return \substr($haystack, -\strlen($needle)) === $needle;
6001
    }
6002
6003
    /**
6004
     * Returns true if the string ends with any of $substrings, false otherwise.
6005
     *
6006
     * - case-sensitive
6007
     *
6008
     * @param string   $str        <p>The input string.</p>
6009
     * @param string[] $substrings <p>Substrings to look for.</p>
6010
     *
6011
     * @psalm-pure
6012
     *
6013
     * @return bool whether or not $str ends with $substring
6014
     */
6015 7
    public static function str_ends_with_any(string $str, array $substrings): bool
6016
    {
6017 7
        if ($substrings === []) {
6018
            return false;
6019
        }
6020
6021 7
        foreach ($substrings as &$substring) {
6022 7
            if (\substr($str, -\strlen($substring)) === $substring) {
6023 7
                return true;
6024
            }
6025
        }
6026
6027 6
        return false;
6028
    }
6029
6030
    /**
6031
     * Ensures that the string begins with $substring. If it doesn't, it's
6032
     * prepended.
6033
     *
6034
     * @param string $str       <p>The input string.</p>
6035
     * @param string $substring <p>The substring to add if not present.</p>
6036
     *
6037
     * @psalm-pure
6038
     *
6039
     * @return string
6040
     */
6041 10
    public static function str_ensure_left(string $str, string $substring): string
6042
    {
6043
        if (
6044 10
            $substring !== ''
6045
            &&
6046 10
            \strpos($str, $substring) === 0
6047
        ) {
6048 6
            return $str;
6049
        }
6050
6051 4
        return $substring . $str;
6052
    }
6053
6054
    /**
6055
     * Ensures that the string ends with $substring. If it doesn't, it's appended.
6056
     *
6057
     * @param string $str       <p>The input string.</p>
6058
     * @param string $substring <p>The substring to add if not present.</p>
6059
     *
6060
     * @psalm-pure
6061
     *
6062
     * @return string
6063
     */
6064 10
    public static function str_ensure_right(string $str, string $substring): string
6065
    {
6066
        if (
6067 10
            $str === ''
6068
            ||
6069 10
            $substring === ''
6070
            ||
6071 10
            \substr($str, -\strlen($substring)) !== $substring
6072
        ) {
6073 4
            $str .= $substring;
6074
        }
6075
6076 10
        return $str;
6077
    }
6078
6079
    /**
6080
     * Capitalizes the first word of the string, replaces underscores with
6081
     * spaces, and strips '_id'.
6082
     *
6083
     * @param string $str
6084
     *
6085
     * @psalm-pure
6086
     *
6087
     * @return string
6088
     */
6089 3
    public static function str_humanize($str): string
6090
    {
6091 3
        $str = \str_replace(
6092
            [
6093 3
                '_id',
6094
                '_',
6095
            ],
6096
            [
6097 3
                '',
6098
                ' ',
6099
            ],
6100 3
            $str
6101
        );
6102
6103 3
        return self::ucfirst(\trim($str));
6104
    }
6105
6106
    /**
6107
     * alias for "UTF8::str_istarts_with()"
6108
     *
6109
     * @param string $haystack
6110
     * @param string $needle
6111
     *
6112
     * @psalm-pure
6113
     *
6114
     * @return bool
6115
     *
6116
     * @see        UTF8::str_istarts_with()
6117
     * @deprecated <p>please use "UTF8::str_istarts_with()"</p>
6118
     */
6119 1
    public static function str_ibegins(string $haystack, string $needle): bool
6120
    {
6121 1
        return self::str_istarts_with($haystack, $needle);
6122
    }
6123
6124
    /**
6125
     * alias for "UTF8::str_iends_with()"
6126
     *
6127
     * @param string $haystack
6128
     * @param string $needle
6129
     *
6130
     * @psalm-pure
6131
     *
6132
     * @return bool
6133
     *
6134
     * @see        UTF8::str_iends_with()
6135
     * @deprecated <p>please use "UTF8::str_iends_with()"</p>
6136
     */
6137 1
    public static function str_iends(string $haystack, string $needle): bool
6138
    {
6139 1
        return self::str_iends_with($haystack, $needle);
6140
    }
6141
6142
    /**
6143
     * Check if the string ends with the given substring, case-insensitive.
6144
     *
6145
     * @param string $haystack <p>The string to search in.</p>
6146
     * @param string $needle   <p>The substring to search for.</p>
6147
     *
6148
     * @psalm-pure
6149
     *
6150
     * @return bool
6151
     */
6152 12
    public static function str_iends_with(string $haystack, string $needle): bool
6153
    {
6154 12
        if ($needle === '') {
6155 2
            return true;
6156
        }
6157
6158 12
        if ($haystack === '') {
6159
            return false;
6160
        }
6161
6162 12
        return self::strcasecmp(\substr($haystack, -\strlen($needle)), $needle) === 0;
6163
    }
6164
6165
    /**
6166
     * Returns true if the string ends with any of $substrings, false otherwise.
6167
     *
6168
     * - case-insensitive
6169
     *
6170
     * @param string   $str        <p>The input string.</p>
6171
     * @param string[] $substrings <p>Substrings to look for.</p>
6172
     *
6173
     * @psalm-pure
6174
     *
6175
     * @return bool
6176
     *              <p>Whether or not $str ends with $substring.</p>
6177
     */
6178 4
    public static function str_iends_with_any(string $str, array $substrings): bool
6179
    {
6180 4
        if ($substrings === []) {
6181
            return false;
6182
        }
6183
6184 4
        foreach ($substrings as &$substring) {
6185 4
            if (self::str_iends_with($str, $substring)) {
6186 4
                return true;
6187
            }
6188
        }
6189
6190
        return false;
6191
    }
6192
6193
    /**
6194
     * Returns the index of the first occurrence of $needle in the string,
6195
     * and false if not found. Accepts an optional offset from which to begin
6196
     * the search.
6197
     *
6198
     * @param string $str      <p>The input string.</p>
6199
     * @param string $needle   <p>Substring to look for.</p>
6200
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6201
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6202
     *
6203
     * @psalm-pure
6204
     *
6205
     * @return false|int
6206
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6207
     *
6208
     * @see        UTF8::stripos()
6209
     * @deprecated <p>please use "UTF8::stripos()"</p>
6210
     */
6211 1
    public static function str_iindex_first(
6212
        string $str,
6213
        string $needle,
6214
        int $offset = 0,
6215
        string $encoding = 'UTF-8'
6216
    ) {
6217 1
        return self::stripos(
6218 1
            $str,
6219 1
            $needle,
6220 1
            $offset,
6221 1
            $encoding
6222
        );
6223
    }
6224
6225
    /**
6226
     * Returns the index of the last occurrence of $needle in the string,
6227
     * and false if not found. Accepts an optional offset from which to begin
6228
     * the search. Offsets may be negative to count from the last character
6229
     * in the string.
6230
     *
6231
     * @param string $str      <p>The input string.</p>
6232
     * @param string $needle   <p>Substring to look for.</p>
6233
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6234
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6235
     *
6236
     * @psalm-pure
6237
     *
6238
     * @return false|int
6239
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6240
     *
6241
     * @see        UTF8::strripos()
6242
     * @deprecated <p>please use "UTF8::strripos()"</p>
6243
     */
6244 10
    public static function str_iindex_last(
6245
        string $str,
6246
        string $needle,
6247
        int $offset = 0,
6248
        string $encoding = 'UTF-8'
6249
    ) {
6250 10
        return self::strripos(
6251 10
            $str,
6252 10
            $needle,
6253 10
            $offset,
6254 10
            $encoding
6255
        );
6256
    }
6257
6258
    /**
6259
     * Returns the index of the first occurrence of $needle in the string,
6260
     * and false if not found. Accepts an optional offset from which to begin
6261
     * the search.
6262
     *
6263
     * @param string $str      <p>The input string.</p>
6264
     * @param string $needle   <p>Substring to look for.</p>
6265
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6266
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6267
     *
6268
     * @psalm-pure
6269
     *
6270
     * @return false|int
6271
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6272
     *
6273
     * @see        UTF8::strpos()
6274
     * @deprecated <p>please use "UTF8::strpos()"</p>
6275
     */
6276 11
    public static function str_index_first(
6277
        string $str,
6278
        string $needle,
6279
        int $offset = 0,
6280
        string $encoding = 'UTF-8'
6281
    ) {
6282 11
        return self::strpos(
6283 11
            $str,
6284 11
            $needle,
6285 11
            $offset,
6286 11
            $encoding
6287
        );
6288
    }
6289
6290
    /**
6291
     * Returns the index of the last occurrence of $needle in the string,
6292
     * and false if not found. Accepts an optional offset from which to begin
6293
     * the search. Offsets may be negative to count from the last character
6294
     * in the string.
6295
     *
6296
     * @param string $str      <p>The input string.</p>
6297
     * @param string $needle   <p>Substring to look for.</p>
6298
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6299
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6300
     *
6301
     * @psalm-pure
6302
     *
6303
     * @return false|int
6304
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6305
     *
6306
     * @see        UTF8::strrpos()
6307
     * @deprecated <p>please use "UTF8::strrpos()"</p>
6308
     */
6309 10
    public static function str_index_last(
6310
        string $str,
6311
        string $needle,
6312
        int $offset = 0,
6313
        string $encoding = 'UTF-8'
6314
    ) {
6315 10
        return self::strrpos(
6316 10
            $str,
6317 10
            $needle,
6318 10
            $offset,
6319 10
            $encoding
6320
        );
6321
    }
6322
6323
    /**
6324
     * Inserts $substring into the string at the $index provided.
6325
     *
6326
     * @param string $str       <p>The input string.</p>
6327
     * @param string $substring <p>String to be inserted.</p>
6328
     * @param int    $index     <p>The index at which to insert the substring.</p>
6329
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
6330
     *
6331
     * @psalm-pure
6332
     *
6333
     * @return string
6334
     */
6335 8
    public static function str_insert(
6336
        string $str,
6337
        string $substring,
6338
        int $index,
6339
        string $encoding = 'UTF-8'
6340
    ): string {
6341 8
        if ($encoding === 'UTF-8') {
6342 4
            $len = (int) \mb_strlen($str);
6343 4
            if ($index > $len) {
6344
                return $str;
6345
            }
6346
6347
            /** @noinspection UnnecessaryCastingInspection */
6348 4
            return (string) \mb_substr($str, 0, $index) .
6349 4
                   $substring .
6350 4
                   (string) \mb_substr($str, $index, $len);
6351
        }
6352
6353 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6354
6355 4
        $len = (int) self::strlen($str, $encoding);
6356 4
        if ($index > $len) {
6357 1
            return $str;
6358
        }
6359
6360 3
        return ((string) self::substr($str, 0, $index, $encoding)) .
6361 3
               $substring .
6362 3
               ((string) self::substr($str, $index, $len, $encoding));
6363
    }
6364
6365
    /**
6366
     * Case-insensitive and UTF-8 safe version of <function>str_replace</function>.
6367
     *
6368
     * @see http://php.net/manual/en/function.str-ireplace.php
6369
     *
6370
     * @param string|string[] $search      <p>
6371
     *                                     Every replacement with search array is
6372
     *                                     performed on the result of previous replacement.
6373
     *                                     </p>
6374
     * @param string|string[] $replacement <p>The replacement.</p>
6375
     * @param mixed           $subject     <p>
6376
     *                                     If subject is an array, then the search and
6377
     *                                     replace is performed with every entry of
6378
     *                                     subject, and the return value is an array as
6379
     *                                     well.
6380
     *                                     </p>
6381
     * @param int             $count       [optional] <p>
6382
     *                                     The number of matched and replaced needles will
6383
     *                                     be returned in count which is passed by
6384
     *                                     reference.
6385
     *                                     </p>
6386
     *
6387
     * @psalm-pure
6388
     *
6389
     * @return mixed a string or an array of replacements
6390
     */
6391 29
    public static function str_ireplace($search, $replacement, $subject, &$count = null)
6392
    {
6393 29
        $search = (array) $search;
6394
6395
        /** @noinspection AlterInForeachInspection */
6396 29
        foreach ($search as &$s) {
6397 29
            $s = (string) $s;
6398 29
            if ($s === '') {
6399 6
                $s = '/^(?<=.)$/';
6400
            } else {
6401 29
                $s = '/' . \preg_quote($s, '/') . '/ui';
6402
            }
6403
        }
6404
6405
        /**
6406
         * @psalm-suppress PossiblyNullArgument
6407
         */
6408 29
        $subject = \preg_replace($search, $replacement, $subject, -1, $count);
6409
6410 29
        return $subject;
6411
    }
6412
6413
    /**
6414
     * Replaces $search from the beginning of string with $replacement.
6415
     *
6416
     * @param string $str         <p>The input string.</p>
6417
     * @param string $search      <p>The string to search for.</p>
6418
     * @param string $replacement <p>The replacement.</p>
6419
     *
6420
     * @psalm-pure
6421
     *
6422
     * @return string string after the replacements
6423
     */
6424 17
    public static function str_ireplace_beginning(string $str, string $search, string $replacement): string
6425
    {
6426 17
        if ($str === '') {
6427 4
            if ($replacement === '') {
6428 2
                return '';
6429
            }
6430
6431 2
            if ($search === '') {
6432 2
                return $replacement;
6433
            }
6434
        }
6435
6436 13
        if ($search === '') {
6437 2
            return $str . $replacement;
6438
        }
6439
6440 11
        if (\stripos($str, $search) === 0) {
6441 10
            return $replacement . \substr($str, \strlen($search));
6442
        }
6443
6444 1
        return $str;
6445
    }
6446
6447
    /**
6448
     * Replaces $search from the ending of string with $replacement.
6449
     *
6450
     * @param string $str         <p>The input string.</p>
6451
     * @param string $search      <p>The string to search for.</p>
6452
     * @param string $replacement <p>The replacement.</p>
6453
     *
6454
     * @psalm-pure
6455
     *
6456
     * @return string
6457
     *                <p>string after the replacements.</p>
6458
     */
6459 17
    public static function str_ireplace_ending(string $str, string $search, string $replacement): string
6460
    {
6461 17
        if ($str === '') {
6462 4
            if ($replacement === '') {
6463 2
                return '';
6464
            }
6465
6466 2
            if ($search === '') {
6467 2
                return $replacement;
6468
            }
6469
        }
6470
6471 13
        if ($search === '') {
6472 2
            return $str . $replacement;
6473
        }
6474
6475 11
        if (\stripos($str, $search, \strlen($str) - \strlen($search)) !== false) {
6476 9
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
6477
        }
6478
6479 11
        return $str;
6480
    }
6481
6482
    /**
6483
     * Check if the string starts with the given substring, case-insensitive.
6484
     *
6485
     * @param string $haystack <p>The string to search in.</p>
6486
     * @param string $needle   <p>The substring to search for.</p>
6487
     *
6488
     * @psalm-pure
6489
     *
6490
     * @return bool
6491
     */
6492 13
    public static function str_istarts_with(string $haystack, string $needle): bool
6493
    {
6494 13
        if ($needle === '') {
6495 2
            return true;
6496
        }
6497
6498 13
        if ($haystack === '') {
6499
            return false;
6500
        }
6501
6502 13
        return self::stripos($haystack, $needle) === 0;
6503
    }
6504
6505
    /**
6506
     * Returns true if the string begins with any of $substrings, false otherwise.
6507
     *
6508
     * - case-insensitive
6509
     *
6510
     * @param string $str        <p>The input string.</p>
6511
     * @param array  $substrings <p>Substrings to look for.</p>
6512
     *
6513
     * @psalm-pure
6514
     *
6515
     * @return bool whether or not $str starts with $substring
6516
     */
6517 5
    public static function str_istarts_with_any(string $str, array $substrings): bool
6518
    {
6519 5
        if ($str === '') {
6520
            return false;
6521
        }
6522
6523 5
        if ($substrings === []) {
6524
            return false;
6525
        }
6526
6527 5
        foreach ($substrings as &$substring) {
6528 5
            if (self::str_istarts_with($str, $substring)) {
6529 5
                return true;
6530
            }
6531
        }
6532
6533 1
        return false;
6534
    }
6535
6536
    /**
6537
     * Gets the substring after the first occurrence of a separator.
6538
     *
6539
     * @param string $str       <p>The input string.</p>
6540
     * @param string $separator <p>The string separator.</p>
6541
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6542
     *
6543
     * @psalm-pure
6544
     *
6545
     * @return string
6546
     */
6547 1
    public static function str_isubstr_after_first_separator(
6548
        string $str,
6549
        string $separator,
6550
        string $encoding = 'UTF-8'
6551
    ): string {
6552 1
        if ($separator === '' || $str === '') {
6553 1
            return '';
6554
        }
6555
6556 1
        $offset = self::stripos($str, $separator);
6557 1
        if ($offset === false) {
6558 1
            return '';
6559
        }
6560
6561 1
        if ($encoding === 'UTF-8') {
6562 1
            return (string) \mb_substr(
6563 1
                $str,
6564 1
                $offset + (int) \mb_strlen($separator)
6565
            );
6566
        }
6567
6568
        return (string) self::substr(
6569
            $str,
6570
            $offset + (int) self::strlen($separator, $encoding),
6571
            null,
6572
            $encoding
6573
        );
6574
    }
6575
6576
    /**
6577
     * Gets the substring after the last occurrence of a separator.
6578
     *
6579
     * @param string $str       <p>The input string.</p>
6580
     * @param string $separator <p>The string separator.</p>
6581
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6582
     *
6583
     * @psalm-pure
6584
     *
6585
     * @return string
6586
     */
6587 1
    public static function str_isubstr_after_last_separator(
6588
        string $str,
6589
        string $separator,
6590
        string $encoding = 'UTF-8'
6591
    ): string {
6592 1
        if ($separator === '' || $str === '') {
6593 1
            return '';
6594
        }
6595
6596 1
        $offset = self::strripos($str, $separator);
6597 1
        if ($offset === false) {
6598 1
            return '';
6599
        }
6600
6601 1
        if ($encoding === 'UTF-8') {
6602 1
            return (string) \mb_substr(
6603 1
                $str,
6604 1
                $offset + (int) self::strlen($separator)
6605
            );
6606
        }
6607
6608
        return (string) self::substr(
6609
            $str,
6610
            $offset + (int) self::strlen($separator, $encoding),
6611
            null,
6612
            $encoding
6613
        );
6614
    }
6615
6616
    /**
6617
     * Gets the substring before the first occurrence of a separator.
6618
     *
6619
     * @param string $str       <p>The input string.</p>
6620
     * @param string $separator <p>The string separator.</p>
6621
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6622
     *
6623
     * @psalm-pure
6624
     *
6625
     * @return string
6626
     */
6627 1
    public static function str_isubstr_before_first_separator(
6628
        string $str,
6629
        string $separator,
6630
        string $encoding = 'UTF-8'
6631
    ): string {
6632 1
        if ($separator === '' || $str === '') {
6633 1
            return '';
6634
        }
6635
6636 1
        $offset = self::stripos($str, $separator);
6637 1
        if ($offset === false) {
6638 1
            return '';
6639
        }
6640
6641 1
        if ($encoding === 'UTF-8') {
6642 1
            return (string) \mb_substr($str, 0, $offset);
6643
        }
6644
6645
        return (string) self::substr($str, 0, $offset, $encoding);
6646
    }
6647
6648
    /**
6649
     * Gets the substring before the last occurrence of a separator.
6650
     *
6651
     * @param string $str       <p>The input string.</p>
6652
     * @param string $separator <p>The string separator.</p>
6653
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6654
     *
6655
     * @psalm-pure
6656
     *
6657
     * @return string
6658
     */
6659 1
    public static function str_isubstr_before_last_separator(
6660
        string $str,
6661
        string $separator,
6662
        string $encoding = 'UTF-8'
6663
    ): string {
6664 1
        if ($separator === '' || $str === '') {
6665 1
            return '';
6666
        }
6667
6668 1
        if ($encoding === 'UTF-8') {
6669 1
            $offset = \mb_strripos($str, $separator);
6670 1
            if ($offset === false) {
6671 1
                return '';
6672
            }
6673
6674 1
            return (string) \mb_substr($str, 0, $offset);
6675
        }
6676
6677
        $offset = self::strripos($str, $separator, 0, $encoding);
6678
        if ($offset === false) {
6679
            return '';
6680
        }
6681
6682
        return (string) self::substr($str, 0, $offset, $encoding);
6683
    }
6684
6685
    /**
6686
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
6687
     *
6688
     * @param string $str           <p>The input string.</p>
6689
     * @param string $needle        <p>The string to look for.</p>
6690
     * @param bool   $before_needle [optional] <p>Default: false</p>
6691
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6692
     *
6693
     * @psalm-pure
6694
     *
6695
     * @return string
6696
     */
6697 2
    public static function str_isubstr_first(
6698
        string $str,
6699
        string $needle,
6700
        bool $before_needle = false,
6701
        string $encoding = 'UTF-8'
6702
    ): string {
6703
        if (
6704 2
            $needle === ''
6705
            ||
6706 2
            $str === ''
6707
        ) {
6708 2
            return '';
6709
        }
6710
6711 2
        $part = self::stristr(
6712 2
            $str,
6713 2
            $needle,
6714 2
            $before_needle,
6715 2
            $encoding
6716
        );
6717 2
        if ($part === false) {
6718 2
            return '';
6719
        }
6720
6721 2
        return $part;
6722
    }
6723
6724
    /**
6725
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
6726
     *
6727
     * @param string $str           <p>The input string.</p>
6728
     * @param string $needle        <p>The string to look for.</p>
6729
     * @param bool   $before_needle [optional] <p>Default: false</p>
6730
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6731
     *
6732
     * @psalm-pure
6733
     *
6734
     * @return string
6735
     */
6736 1
    public static function str_isubstr_last(
6737
        string $str,
6738
        string $needle,
6739
        bool $before_needle = false,
6740
        string $encoding = 'UTF-8'
6741
    ): string {
6742
        if (
6743 1
            $needle === ''
6744
            ||
6745 1
            $str === ''
6746
        ) {
6747 1
            return '';
6748
        }
6749
6750 1
        $part = self::strrichr(
6751 1
            $str,
6752 1
            $needle,
6753 1
            $before_needle,
6754 1
            $encoding
6755
        );
6756 1
        if ($part === false) {
6757 1
            return '';
6758
        }
6759
6760 1
        return $part;
6761
    }
6762
6763
    /**
6764
     * Returns the last $n characters of the string.
6765
     *
6766
     * @param string $str      <p>The input string.</p>
6767
     * @param int    $n        <p>Number of characters to retrieve from the end.</p>
6768
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6769
     *
6770
     * @psalm-pure
6771
     *
6772
     * @return string
6773
     */
6774 12
    public static function str_last_char(
6775
        string $str,
6776
        int $n = 1,
6777
        string $encoding = 'UTF-8'
6778
    ): string {
6779 12
        if ($str === '' || $n <= 0) {
6780 4
            return '';
6781
        }
6782
6783 8
        if ($encoding === 'UTF-8') {
6784 4
            return (string) \mb_substr($str, -$n);
6785
        }
6786
6787 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6788
6789 4
        return (string) self::substr($str, -$n, null, $encoding);
6790
    }
6791
6792
    /**
6793
     * Limit the number of characters in a string.
6794
     *
6795
     * @param string $str        <p>The input string.</p>
6796
     * @param int    $length     [optional] <p>Default: 100</p>
6797
     * @param string $str_add_on [optional] <p>Default: …</p>
6798
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6799
     *
6800
     * @psalm-pure
6801
     *
6802
     * @return string
6803
     */
6804 2
    public static function str_limit(
6805
        string $str,
6806
        int $length = 100,
6807
        string $str_add_on = '…',
6808
        string $encoding = 'UTF-8'
6809
    ): string {
6810 2
        if ($str === '' || $length <= 0) {
6811 2
            return '';
6812
        }
6813
6814 2
        if ($encoding === 'UTF-8') {
6815 2
            if ((int) \mb_strlen($str) <= $length) {
6816 2
                return $str;
6817
            }
6818
6819
            /** @noinspection UnnecessaryCastingInspection */
6820 2
            return (string) \mb_substr($str, 0, $length - (int) self::strlen($str_add_on)) . $str_add_on;
6821
        }
6822
6823
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6824
6825
        if ((int) self::strlen($str, $encoding) <= $length) {
6826
            return $str;
6827
        }
6828
6829
        return ((string) self::substr($str, 0, $length - (int) self::strlen($str_add_on), $encoding)) . $str_add_on;
6830
    }
6831
6832
    /**
6833
     * Limit the number of characters in a string, but also after the next word.
6834
     *
6835
     * @param string $str        <p>The input string.</p>
6836
     * @param int    $length     [optional] <p>Default: 100</p>
6837
     * @param string $str_add_on [optional] <p>Default: …</p>
6838
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6839
     *
6840
     * @psalm-pure
6841
     *
6842
     * @return string
6843
     */
6844 6
    public static function str_limit_after_word(
6845
        string $str,
6846
        int $length = 100,
6847
        string $str_add_on = '…',
6848
        string $encoding = 'UTF-8'
6849
    ): string {
6850 6
        if ($str === '' || $length <= 0) {
6851 2
            return '';
6852
        }
6853
6854 6
        if ($encoding === 'UTF-8') {
6855
            /** @noinspection UnnecessaryCastingInspection */
6856 2
            if ((int) \mb_strlen($str) <= $length) {
6857 2
                return $str;
6858
            }
6859
6860 2
            if (\mb_substr($str, $length - 1, 1) === ' ') {
6861 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6862
            }
6863
6864 2
            $str = \mb_substr($str, 0, $length);
6865
6866 2
            $array = \explode(' ', $str, -1);
6867 2
            $new_str = \implode(' ', $array);
6868
6869 2
            if ($new_str === '') {
6870 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6871
            }
6872
        } else {
6873 4
            if ((int) self::strlen($str, $encoding) <= $length) {
6874
                return $str;
6875
            }
6876
6877 4
            if (self::substr($str, $length - 1, 1, $encoding) === ' ') {
6878 3
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6879
            }
6880
6881
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6882 1
            $str = self::substr($str, 0, $length, $encoding);
6883
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6884 1
            if ($str === false) {
6885
                return '' . $str_add_on;
6886
            }
6887
6888 1
            $array = \explode(' ', $str, -1);
6889 1
            $new_str = \implode(' ', $array);
6890
6891 1
            if ($new_str === '') {
6892
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6893
            }
6894
        }
6895
6896 3
        return $new_str . $str_add_on;
6897
    }
6898
6899
    /**
6900
     * Returns the longest common prefix between the $str1 and $str2.
6901
     *
6902
     * @param string $str1     <p>The input sting.</p>
6903
     * @param string $str2     <p>Second string for comparison.</p>
6904
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6905
     *
6906
     * @psalm-pure
6907
     *
6908
     * @return string
6909
     */
6910 10
    public static function str_longest_common_prefix(
6911
        string $str1,
6912
        string $str2,
6913
        string $encoding = 'UTF-8'
6914
    ): string {
6915
        // init
6916 10
        $longest_common_prefix = '';
6917
6918 10
        if ($encoding === 'UTF-8') {
6919 5
            $max_length = (int) \min(
6920 5
                \mb_strlen($str1),
6921 5
                \mb_strlen($str2)
6922
            );
6923
6924 5
            for ($i = 0; $i < $max_length; ++$i) {
6925 4
                $char = \mb_substr($str1, $i, 1);
6926
6927
                if (
6928 4
                    $char !== false
6929
                    &&
6930 4
                    $char === \mb_substr($str2, $i, 1)
6931
                ) {
6932 3
                    $longest_common_prefix .= $char;
6933
                } else {
6934 3
                    break;
6935
                }
6936
            }
6937
        } else {
6938 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6939
6940 5
            $max_length = (int) \min(
6941 5
                self::strlen($str1, $encoding),
6942 5
                self::strlen($str2, $encoding)
6943
            );
6944
6945 5
            for ($i = 0; $i < $max_length; ++$i) {
6946 4
                $char = self::substr($str1, $i, 1, $encoding);
6947
6948
                if (
6949 4
                    $char !== false
6950
                    &&
6951 4
                    $char === self::substr($str2, $i, 1, $encoding)
6952
                ) {
6953 3
                    $longest_common_prefix .= $char;
6954
                } else {
6955 3
                    break;
6956
                }
6957
            }
6958
        }
6959
6960 10
        return $longest_common_prefix;
6961
    }
6962
6963
    /**
6964
     * Returns the longest common substring between the $str1 and $str2.
6965
     * In the case of ties, it returns that which occurs first.
6966
     *
6967
     * @param string $str1
6968
     * @param string $str2     <p>Second string for comparison.</p>
6969
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6970
     *
6971
     * @psalm-pure
6972
     *
6973
     * @return string
6974
     *                <p>A string with its $str being the longest common substring.</p>
6975
     */
6976 11
    public static function str_longest_common_substring(
6977
        string $str1,
6978
        string $str2,
6979
        string $encoding = 'UTF-8'
6980
    ): string {
6981 11
        if ($str1 === '' || $str2 === '') {
6982 2
            return '';
6983
        }
6984
6985
        // Uses dynamic programming to solve
6986
        // http://en.wikipedia.org/wiki/Longest_common_substring_problem
6987
6988 9
        if ($encoding === 'UTF-8') {
6989 4
            $str_length = (int) \mb_strlen($str1);
6990 4
            $other_length = (int) \mb_strlen($str2);
6991
        } else {
6992 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6993
6994 5
            $str_length = (int) self::strlen($str1, $encoding);
6995 5
            $other_length = (int) self::strlen($str2, $encoding);
6996
        }
6997
6998
        // Return if either string is empty
6999 9
        if ($str_length === 0 || $other_length === 0) {
7000
            return '';
7001
        }
7002
7003 9
        $len = 0;
7004 9
        $end = 0;
7005 9
        $table = \array_fill(
7006 9
            0,
7007 9
            $str_length + 1,
7008 9
            \array_fill(0, $other_length + 1, 0)
7009
        );
7010
7011 9
        if ($encoding === 'UTF-8') {
7012 9
            for ($i = 1; $i <= $str_length; ++$i) {
7013 9
                for ($j = 1; $j <= $other_length; ++$j) {
7014 9
                    $str_char = \mb_substr($str1, $i - 1, 1);
7015 9
                    $other_char = \mb_substr($str2, $j - 1, 1);
7016
7017 9
                    if ($str_char === $other_char) {
7018 8
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
7019 8
                        if ($table[$i][$j] > $len) {
7020 8
                            $len = $table[$i][$j];
7021 8
                            $end = $i;
7022
                        }
7023
                    } else {
7024 9
                        $table[$i][$j] = 0;
7025
                    }
7026
                }
7027
            }
7028
        } else {
7029
            for ($i = 1; $i <= $str_length; ++$i) {
7030
                for ($j = 1; $j <= $other_length; ++$j) {
7031
                    $str_char = self::substr($str1, $i - 1, 1, $encoding);
7032
                    $other_char = self::substr($str2, $j - 1, 1, $encoding);
7033
7034
                    if ($str_char === $other_char) {
7035
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
7036
                        if ($table[$i][$j] > $len) {
7037
                            $len = $table[$i][$j];
7038
                            $end = $i;
7039
                        }
7040
                    } else {
7041
                        $table[$i][$j] = 0;
7042
                    }
7043
                }
7044
            }
7045
        }
7046
7047 9
        if ($encoding === 'UTF-8') {
7048 9
            return (string) \mb_substr($str1, $end - $len, $len);
7049
        }
7050
7051
        return (string) self::substr($str1, $end - $len, $len, $encoding);
7052
    }
7053
7054
    /**
7055
     * Returns the longest common suffix between the $str1 and $str2.
7056
     *
7057
     * @param string $str1
7058
     * @param string $str2     <p>Second string for comparison.</p>
7059
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7060
     *
7061
     * @psalm-pure
7062
     *
7063
     * @return string
7064
     */
7065 10
    public static function str_longest_common_suffix(
7066
        string $str1,
7067
        string $str2,
7068
        string $encoding = 'UTF-8'
7069
    ): string {
7070 10
        if ($str1 === '' || $str2 === '') {
7071 2
            return '';
7072
        }
7073
7074 8
        if ($encoding === 'UTF-8') {
7075 4
            $max_length = (int) \min(
7076 4
                \mb_strlen($str1, $encoding),
7077 4
                \mb_strlen($str2, $encoding)
7078
            );
7079
7080 4
            $longest_common_suffix = '';
7081 4
            for ($i = 1; $i <= $max_length; ++$i) {
7082 4
                $char = \mb_substr($str1, -$i, 1);
7083
7084
                if (
7085 4
                    $char !== false
7086
                    &&
7087 4
                    $char === \mb_substr($str2, -$i, 1)
7088
                ) {
7089 3
                    $longest_common_suffix = $char . $longest_common_suffix;
7090
                } else {
7091 3
                    break;
7092
                }
7093
            }
7094
        } else {
7095 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7096
7097 4
            $max_length = (int) \min(
7098 4
                self::strlen($str1, $encoding),
7099 4
                self::strlen($str2, $encoding)
7100
            );
7101
7102 4
            $longest_common_suffix = '';
7103 4
            for ($i = 1; $i <= $max_length; ++$i) {
7104 4
                $char = self::substr($str1, -$i, 1, $encoding);
7105
7106
                if (
7107 4
                    $char !== false
7108
                    &&
7109 4
                    $char === self::substr($str2, -$i, 1, $encoding)
7110
                ) {
7111 3
                    $longest_common_suffix = $char . $longest_common_suffix;
7112
                } else {
7113 3
                    break;
7114
                }
7115
            }
7116
        }
7117
7118 8
        return $longest_common_suffix;
7119
    }
7120
7121
    /**
7122
     * Returns true if $str matches the supplied pattern, false otherwise.
7123
     *
7124
     * @param string $str     <p>The input string.</p>
7125
     * @param string $pattern <p>Regex pattern to match against.</p>
7126
     *
7127
     * @psalm-pure
7128
     *
7129
     * @return bool whether or not $str matches the pattern
7130
     */
7131 10
    public static function str_matches_pattern(string $str, string $pattern): bool
7132
    {
7133 10
        return (bool) \preg_match('/' . $pattern . '/u', $str);
7134
    }
7135
7136
    /**
7137
     * Returns whether or not a character exists at an index. Offsets may be
7138
     * negative to count from the last character in the string. Implements
7139
     * part of the ArrayAccess interface.
7140
     *
7141
     * @param string $str      <p>The input string.</p>
7142
     * @param int    $offset   <p>The index to check.</p>
7143
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7144
     *
7145
     * @psalm-pure
7146
     *
7147
     * @return bool whether or not the index exists
7148
     */
7149 6
    public static function str_offset_exists(string $str, int $offset, string $encoding = 'UTF-8'): bool
7150
    {
7151
        // init
7152 6
        $length = (int) self::strlen($str, $encoding);
7153
7154 6
        if ($offset >= 0) {
7155 3
            return $length > $offset;
7156
        }
7157
7158 3
        return $length >= \abs($offset);
7159
    }
7160
7161
    /**
7162
     * Returns the character at the given index. Offsets may be negative to
7163
     * count from the last character in the string. Implements part of the
7164
     * ArrayAccess interface, and throws an OutOfBoundsException if the index
7165
     * does not exist.
7166
     *
7167
     * @param string $str      <p>The input string.</p>
7168
     * @param int    $index    <p>The <strong>index</strong> from which to retrieve the char.</p>
7169
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7170
     *
7171
     * @throws \OutOfBoundsException if the positive or negative offset does not exist
7172
     *
7173
     * @return string
7174
     *                <p>The character at the specified index.</p>
7175
     *
7176
     * @psalm-pure
7177
     */
7178 2
    public static function str_offset_get(string $str, int $index, string $encoding = 'UTF-8'): string
7179
    {
7180
        // init
7181 2
        $length = (int) self::strlen($str);
7182
7183
        if (
7184 2
            ($index >= 0 && $length <= $index)
7185
            ||
7186 2
            $length < \abs($index)
7187
        ) {
7188 1
            throw new \OutOfBoundsException('No character exists at the index');
7189
        }
7190
7191 1
        return self::char_at($str, $index, $encoding);
7192
    }
7193
7194
    /**
7195
     * Pad a UTF-8 string to a given length with another string.
7196
     *
7197
     * @param string     $str        <p>The input string.</p>
7198
     * @param int        $pad_length <p>The length of return string.</p>
7199
     * @param string     $pad_string [optional] <p>String to use for padding the input string.</p>
7200
     * @param int|string $pad_type   [optional] <p>
7201
     *                               Can be <strong>STR_PAD_RIGHT</strong> (default), [or string "right"]<br>
7202
     *                               <strong>STR_PAD_LEFT</strong> [or string "left"] or<br>
7203
     *                               <strong>STR_PAD_BOTH</strong> [or string "both"]
7204
     *                               </p>
7205
     * @param string     $encoding   [optional] <p>Default: 'UTF-8'</p>
7206
     *
7207
     * @psalm-pure
7208
     *
7209
     * @return string
7210
     *                <p>Returns the padded string.</p>
7211
     */
7212 41
    public static function str_pad(
7213
        string $str,
7214
        int $pad_length,
7215
        string $pad_string = ' ',
7216
        $pad_type = \STR_PAD_RIGHT,
7217
        string $encoding = 'UTF-8'
7218
    ): string {
7219 41
        if ($pad_length === 0 || $pad_string === '') {
7220 1
            return $str;
7221
        }
7222
7223 41
        if ($pad_type !== (int) $pad_type) {
7224 13
            if ($pad_type === 'left') {
7225 3
                $pad_type = \STR_PAD_LEFT;
7226 10
            } elseif ($pad_type === 'right') {
7227 6
                $pad_type = \STR_PAD_RIGHT;
7228 4
            } elseif ($pad_type === 'both') {
7229 3
                $pad_type = \STR_PAD_BOTH;
7230
            } else {
7231 1
                throw new \InvalidArgumentException(
7232 1
                    'Pad expects $pad_type to be "STR_PAD_*" or ' . "to be one of 'left', 'right' or 'both'"
7233
                );
7234
            }
7235
        }
7236
7237 40
        if ($encoding === 'UTF-8') {
7238 25
            $str_length = (int) \mb_strlen($str);
7239
7240 25
            if ($pad_length >= $str_length) {
7241
                switch ($pad_type) {
7242 25
                    case \STR_PAD_LEFT:
7243 8
                        $ps_length = (int) \mb_strlen($pad_string);
7244
7245 8
                        $diff = ($pad_length - $str_length);
7246
7247 8
                        $pre = (string) \mb_substr(
7248 8
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7249 8
                            0,
7250 8
                            $diff
7251
                        );
7252 8
                        $post = '';
7253
7254 8
                        break;
7255
7256 20
                    case \STR_PAD_BOTH:
7257 14
                        $diff = ($pad_length - $str_length);
7258
7259 14
                        $ps_length_left = (int) \floor($diff / 2);
7260
7261 14
                        $ps_length_right = (int) \ceil($diff / 2);
7262
7263 14
                        $pre = (string) \mb_substr(
7264 14
                            \str_repeat($pad_string, $ps_length_left),
7265 14
                            0,
7266 14
                            $ps_length_left
7267
                        );
7268 14
                        $post = (string) \mb_substr(
7269 14
                            \str_repeat($pad_string, $ps_length_right),
7270 14
                            0,
7271 14
                            $ps_length_right
7272
                        );
7273
7274 14
                        break;
7275
7276 9
                    case \STR_PAD_RIGHT:
7277
                    default:
7278 9
                        $ps_length = (int) \mb_strlen($pad_string);
7279
7280 9
                        $diff = ($pad_length - $str_length);
7281
7282 9
                        $post = (string) \mb_substr(
7283 9
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7284 9
                            0,
7285 9
                            $diff
7286
                        );
7287 9
                        $pre = '';
7288
                }
7289
7290 25
                return $pre . $str . $post;
7291
            }
7292
7293 3
            return $str;
7294
        }
7295
7296 15
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7297
7298 15
        $str_length = (int) self::strlen($str, $encoding);
7299
7300 15
        if ($pad_length >= $str_length) {
7301
            switch ($pad_type) {
7302 14
                case \STR_PAD_LEFT:
7303 5
                    $ps_length = (int) self::strlen($pad_string, $encoding);
7304
7305 5
                    $diff = ($pad_length - $str_length);
7306
7307 5
                    $pre = (string) self::substr(
7308 5
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7309 5
                        0,
7310 5
                        $diff,
7311 5
                        $encoding
7312
                    );
7313 5
                    $post = '';
7314
7315 5
                    break;
7316
7317 9
                case \STR_PAD_BOTH:
7318 3
                    $diff = ($pad_length - $str_length);
7319
7320 3
                    $ps_length_left = (int) \floor($diff / 2);
7321
7322 3
                    $ps_length_right = (int) \ceil($diff / 2);
7323
7324 3
                    $pre = (string) self::substr(
7325 3
                        \str_repeat($pad_string, $ps_length_left),
7326 3
                        0,
7327 3
                        $ps_length_left,
7328 3
                        $encoding
7329
                    );
7330 3
                    $post = (string) self::substr(
7331 3
                        \str_repeat($pad_string, $ps_length_right),
7332 3
                        0,
7333 3
                        $ps_length_right,
7334 3
                        $encoding
7335
                    );
7336
7337 3
                    break;
7338
7339 6
                case \STR_PAD_RIGHT:
7340
                default:
7341 6
                    $ps_length = (int) self::strlen($pad_string, $encoding);
7342
7343 6
                    $diff = ($pad_length - $str_length);
7344
7345 6
                    $post = (string) self::substr(
7346 6
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7347 6
                        0,
7348 6
                        $diff,
7349 6
                        $encoding
7350
                    );
7351 6
                    $pre = '';
7352
            }
7353
7354 14
            return $pre . $str . $post;
7355
        }
7356
7357 1
        return $str;
7358
    }
7359
7360
    /**
7361
     * Returns a new string of a given length such that both sides of the
7362
     * string are padded. Alias for "UTF8::str_pad()" with a $pad_type of 'both'.
7363
     *
7364
     * @param string $str
7365
     * @param int    $length   <p>Desired string length after padding.</p>
7366
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7367
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7368
     *
7369
     * @psalm-pure
7370
     *
7371
     * @return string
7372
     *                <p>The string with padding applied.</p>
7373
     */
7374 11
    public static function str_pad_both(
7375
        string $str,
7376
        int $length,
7377
        string $pad_str = ' ',
7378
        string $encoding = 'UTF-8'
7379
    ): string {
7380 11
        return self::str_pad(
7381 11
            $str,
7382 11
            $length,
7383 11
            $pad_str,
7384 11
            \STR_PAD_BOTH,
7385 11
            $encoding
7386
        );
7387
    }
7388
7389
    /**
7390
     * Returns a new string of a given length such that the beginning of the
7391
     * string is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'left'.
7392
     *
7393
     * @param string $str
7394
     * @param int    $length   <p>Desired string length after padding.</p>
7395
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7396
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7397
     *
7398
     * @psalm-pure
7399
     *
7400
     * @return string
7401
     *                <p>The string with left padding.</p>
7402
     */
7403 7
    public static function str_pad_left(
7404
        string $str,
7405
        int $length,
7406
        string $pad_str = ' ',
7407
        string $encoding = 'UTF-8'
7408
    ): string {
7409 7
        return self::str_pad(
7410 7
            $str,
7411 7
            $length,
7412 7
            $pad_str,
7413 7
            \STR_PAD_LEFT,
7414 7
            $encoding
7415
        );
7416
    }
7417
7418
    /**
7419
     * Returns a new string of a given length such that the end of the string
7420
     * is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'right'.
7421
     *
7422
     * @param string $str
7423
     * @param int    $length   <p>Desired string length after padding.</p>
7424
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7425
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7426
     *
7427
     * @psalm-pure
7428
     *
7429
     * @return string
7430
     *                <p>The string with right padding.</p>
7431
     */
7432 7
    public static function str_pad_right(
7433
        string $str,
7434
        int $length,
7435
        string $pad_str = ' ',
7436
        string $encoding = 'UTF-8'
7437
    ): string {
7438 7
        return self::str_pad(
7439 7
            $str,
7440 7
            $length,
7441 7
            $pad_str,
7442 7
            \STR_PAD_RIGHT,
7443 7
            $encoding
7444
        );
7445
    }
7446
7447
    /**
7448
     * Repeat a string.
7449
     *
7450
     * @param string $str        <p>
7451
     *                           The string to be repeated.
7452
     *                           </p>
7453
     * @param int    $multiplier <p>
7454
     *                           Number of time the input string should be
7455
     *                           repeated.
7456
     *                           </p>
7457
     *                           <p>
7458
     *                           multiplier has to be greater than or equal to 0.
7459
     *                           If the multiplier is set to 0, the function
7460
     *                           will return an empty string.
7461
     *                           </p>
7462
     *
7463
     * @psalm-pure
7464
     *
7465
     * @return string
7466
     *                <p>The repeated string.</p>
7467
     */
7468 9
    public static function str_repeat(string $str, int $multiplier): string
7469
    {
7470 9
        $str = self::filter($str);
7471
7472 9
        return \str_repeat($str, $multiplier);
7473
    }
7474
7475
    /**
7476
     * INFO: This is only a wrapper for "str_replace()"  -> the original functions is already UTF-8 safe.
7477
     *
7478
     * Replace all occurrences of the search string with the replacement string
7479
     *
7480
     * @see http://php.net/manual/en/function.str-replace.php
7481
     *
7482
     * @param mixed $search  <p>
7483
     *                       The value being searched for, otherwise known as the needle.
7484
     *                       An array may be used to designate multiple needles.
7485
     *                       </p>
7486
     * @param mixed $replace <p>
7487
     *                       The replacement value that replaces found search
7488
     *                       values. An array may be used to designate multiple replacements.
7489
     *                       </p>
7490
     * @param mixed $subject <p>
7491
     *                       The string or array being searched and replaced on,
7492
     *                       otherwise known as the haystack.
7493
     *                       </p>
7494
     *                       <p>
7495
     *                       If subject is an array, then the search and
7496
     *                       replace is performed with every entry of
7497
     *                       subject, and the return value is an array as
7498
     *                       well.
7499
     *                       </p>
7500
     * @param int   $count   [optional] If passed, this will hold the number of matched and replaced needles
7501
     *
7502
     * @psalm-pure
7503
     *
7504
     * @return mixed this function returns a string or an array with the replaced values
7505
     */
7506 12
    public static function str_replace(
7507
        $search,
7508
        $replace,
7509
        $subject,
7510
        int &$count = null
7511
    ) {
7512
        /**
7513
         * @psalm-suppress PossiblyNullArgument
7514
         */
7515 12
        return \str_replace(
7516 12
            $search,
7517 12
            $replace,
7518 12
            $subject,
7519 12
            $count
7520
        );
7521
    }
7522
7523
    /**
7524
     * Replaces $search from the beginning of string with $replacement.
7525
     *
7526
     * @param string $str         <p>The input string.</p>
7527
     * @param string $search      <p>The string to search for.</p>
7528
     * @param string $replacement <p>The replacement.</p>
7529
     *
7530
     * @psalm-pure
7531
     *
7532
     * @return string
7533
     *                <p>A string after the replacements.</p>
7534
     */
7535 17
    public static function str_replace_beginning(
7536
        string $str,
7537
        string $search,
7538
        string $replacement
7539
    ): string {
7540 17
        if ($str === '') {
7541 4
            if ($replacement === '') {
7542 2
                return '';
7543
            }
7544
7545 2
            if ($search === '') {
7546 2
                return $replacement;
7547
            }
7548
        }
7549
7550 13
        if ($search === '') {
7551 2
            return $str . $replacement;
7552
        }
7553
7554 11
        if (\strpos($str, $search) === 0) {
7555 9
            return $replacement . \substr($str, \strlen($search));
7556
        }
7557
7558 2
        return $str;
7559
    }
7560
7561
    /**
7562
     * Replaces $search from the ending of string with $replacement.
7563
     *
7564
     * @param string $str         <p>The input string.</p>
7565
     * @param string $search      <p>The string to search for.</p>
7566
     * @param string $replacement <p>The replacement.</p>
7567
     *
7568
     * @psalm-pure
7569
     *
7570
     * @return string
7571
     *                <p>A string after the replacements.</p>
7572
     */
7573 17
    public static function str_replace_ending(
7574
        string $str,
7575
        string $search,
7576
        string $replacement
7577
    ): string {
7578 17
        if ($str === '') {
7579 4
            if ($replacement === '') {
7580 2
                return '';
7581
            }
7582
7583 2
            if ($search === '') {
7584 2
                return $replacement;
7585
            }
7586
        }
7587
7588 13
        if ($search === '') {
7589 2
            return $str . $replacement;
7590
        }
7591
7592 11
        if (\strpos($str, $search, \strlen($str) - \strlen($search)) !== false) {
7593 8
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
7594
        }
7595
7596 11
        return $str;
7597
    }
7598
7599
    /**
7600
     * Replace the first "$search"-term with the "$replace"-term.
7601
     *
7602
     * @param string $search
7603
     * @param string $replace
7604
     * @param string $subject
7605
     *
7606
     * @psalm-pure
7607
     *
7608
     * @return string
7609
     *
7610
     * @psalm-suppress InvalidReturnType
7611
     */
7612 2
    public static function str_replace_first(
7613
        string $search,
7614
        string $replace,
7615
        string $subject
7616
    ): string {
7617 2
        $pos = self::strpos($subject, $search);
7618
7619 2
        if ($pos !== false) {
7620
            /**
7621
             * @psalm-suppress InvalidReturnStatement
7622
             */
7623 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...
7624 2
                $subject,
7625 2
                $replace,
7626 2
                $pos,
7627 2
                (int) self::strlen($search)
7628
            );
7629
        }
7630
7631 2
        return $subject;
7632
    }
7633
7634
    /**
7635
     * Replace the last "$search"-term with the "$replace"-term.
7636
     *
7637
     * @param string $search
7638
     * @param string $replace
7639
     * @param string $subject
7640
     *
7641
     * @psalm-pure
7642
     *
7643
     * @return string
7644
     *
7645
     * @psalm-suppress InvalidReturnType
7646
     */
7647 2
    public static function str_replace_last(
7648
        string $search,
7649
        string $replace,
7650
        string $subject
7651
    ): string {
7652 2
        $pos = self::strrpos($subject, $search);
7653 2
        if ($pos !== false) {
7654
            /**
7655
             * @psalm-suppress InvalidReturnStatement
7656
             */
7657 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...
7658 2
                $subject,
7659 2
                $replace,
7660 2
                $pos,
7661 2
                (int) self::strlen($search)
7662
            );
7663
        }
7664
7665 2
        return $subject;
7666
    }
7667
7668
    /**
7669
     * Shuffles all the characters in the string.
7670
     *
7671
     * PS: uses random algorithm which is weak for cryptography purposes
7672
     *
7673
     * @param string $str      <p>The input string</p>
7674
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7675
     *
7676
     * @return string
7677
     *                <p>The shuffled string.</p>
7678
     */
7679 5
    public static function str_shuffle(string $str, string $encoding = 'UTF-8'): string
7680
    {
7681 5
        if ($encoding === 'UTF-8') {
7682 5
            $indexes = \range(0, (int) \mb_strlen($str) - 1);
7683
            /** @noinspection NonSecureShuffleUsageInspection */
7684 5
            \shuffle($indexes);
7685
7686
            // init
7687 5
            $shuffled_str = '';
7688
7689 5
            foreach ($indexes as &$i) {
7690 5
                $tmp_sub_str = \mb_substr($str, $i, 1);
7691 5
                if ($tmp_sub_str !== false) {
7692 5
                    $shuffled_str .= $tmp_sub_str;
7693
                }
7694
            }
7695
        } else {
7696
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7697
7698
            $indexes = \range(0, (int) self::strlen($str, $encoding) - 1);
7699
            /** @noinspection NonSecureShuffleUsageInspection */
7700
            \shuffle($indexes);
7701
7702
            // init
7703
            $shuffled_str = '';
7704
7705
            foreach ($indexes as &$i) {
7706
                $tmp_sub_str = self::substr($str, $i, 1, $encoding);
7707
                if ($tmp_sub_str !== false) {
7708
                    $shuffled_str .= $tmp_sub_str;
7709
                }
7710
            }
7711
        }
7712
7713 5
        return $shuffled_str;
7714
    }
7715
7716
    /**
7717
     * Returns the substring beginning at $start, and up to, but not including
7718
     * the index specified by $end. If $end is omitted, the function extracts
7719
     * the remaining string. If $end is negative, it is computed from the end
7720
     * of the string.
7721
     *
7722
     * @param string $str
7723
     * @param int    $start    <p>Initial index from which to begin extraction.</p>
7724
     * @param int    $end      [optional] <p>Index at which to end extraction. Default: null</p>
7725
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7726
     *
7727
     * @psalm-pure
7728
     *
7729
     * @return false|string
7730
     *                      <p>The extracted substring.</p><p>If <i>str</i> is shorter than <i>start</i>
7731
     *                      characters long, <b>FALSE</b> will be returned.
7732
     */
7733 18
    public static function str_slice(
7734
        string $str,
7735
        int $start,
7736
        int $end = null,
7737
        string $encoding = 'UTF-8'
7738
    ) {
7739 18
        if ($encoding === 'UTF-8') {
7740 7
            if ($end === null) {
7741 1
                $length = (int) \mb_strlen($str);
7742 6
            } elseif ($end >= 0 && $end <= $start) {
7743 2
                return '';
7744 4
            } elseif ($end < 0) {
7745 1
                $length = (int) \mb_strlen($str) + $end - $start;
7746
            } else {
7747 3
                $length = $end - $start;
7748
            }
7749
7750 5
            return \mb_substr($str, $start, $length);
7751
        }
7752
7753 11
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7754
7755 11
        if ($end === null) {
7756 5
            $length = (int) self::strlen($str, $encoding);
7757 6
        } elseif ($end >= 0 && $end <= $start) {
7758 2
            return '';
7759 4
        } elseif ($end < 0) {
7760 1
            $length = (int) self::strlen($str, $encoding) + $end - $start;
7761
        } else {
7762 3
            $length = $end - $start;
7763
        }
7764
7765 9
        return self::substr($str, $start, $length, $encoding);
7766
    }
7767
7768
    /**
7769
     * Convert a string to e.g.: "snake_case"
7770
     *
7771
     * @param string $str
7772
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7773
     *
7774
     * @psalm-pure
7775
     *
7776
     * @return string
7777
     *                <p>A string in snake_case.</p>
7778
     */
7779 22
    public static function str_snakeize(string $str, string $encoding = 'UTF-8'): string
7780
    {
7781 22
        if ($str === '') {
7782
            return '';
7783
        }
7784
7785 22
        $str = \str_replace(
7786 22
            '-',
7787 22
            '_',
7788 22
            self::normalize_whitespace($str)
7789
        );
7790
7791 22
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
7792 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7793
        }
7794
7795 22
        $str = (string) \preg_replace_callback(
7796 22
            '/([\\p{N}|\\p{Lu}])/u',
7797
            /**
7798
             * @param string[] $matches
7799
             *
7800
             * @psalm-pure
7801
             *
7802
             * @return string
7803
             */
7804
            static function (array $matches) use ($encoding): string {
7805 9
                $match = $matches[1];
7806 9
                $match_int = (int) $match;
7807
7808 9
                if ((string) $match_int === $match) {
7809 4
                    return '_' . $match . '_';
7810
                }
7811
7812 5
                if ($encoding === 'UTF-8') {
7813 5
                    return '_' . \mb_strtolower($match);
7814
                }
7815
7816
                return '_' . self::strtolower($match, $encoding);
7817 22
            },
7818 22
            $str
7819
        );
7820
7821 22
        $str = (string) \preg_replace(
7822
            [
7823 22
                '/\\s+/u',           // convert spaces to "_"
7824
                '/^\\s+|\\s+$/u', // trim leading & trailing spaces
7825
                '/_+/',                 // remove double "_"
7826
            ],
7827
            [
7828 22
                '_',
7829
                '',
7830
                '_',
7831
            ],
7832 22
            $str
7833
        );
7834
7835 22
        return \trim(\trim($str, '_')); // trim leading & trailing "_" + whitespace
7836
    }
7837
7838
    /**
7839
     * Sort all characters according to code points.
7840
     *
7841
     * @param string $str    <p>A UTF-8 string.</p>
7842
     * @param bool   $unique <p>Sort unique. If <strong>true</strong>, repeated characters are ignored.</p>
7843
     * @param bool   $desc   <p>If <strong>true</strong>, will sort characters in reverse code point order.</p>
7844
     *
7845
     * @psalm-pure
7846
     *
7847
     * @return string
7848
     *                <p>A string of sorted characters.</p>
7849
     */
7850 2
    public static function str_sort(string $str, bool $unique = false, bool $desc = false): string
7851
    {
7852 2
        $array = self::codepoints($str);
7853
7854 2
        if ($unique) {
7855 2
            $array = \array_flip(\array_flip($array));
7856
        }
7857
7858 2
        if ($desc) {
7859 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

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

7861
            \asort(/** @scrutinizer ignore-type */ $array);
Loading history...
7862
        }
7863
7864 2
        return self::string($array);
7865
    }
7866
7867
    /**
7868
     * Convert a string to an array of Unicode characters.
7869
     *
7870
     * @param int[]|string[] $input                   <p>The string[] or int[] to split into array.</p>
7871
     * @param int            $length                  [optional] <p>Max character length of each array
7872
     *                                                lement.</p>
7873
     * @param bool           $clean_utf8              [optional] <p>Remove non UTF-8 chars from the
7874
     *                                                string.</p>
7875
     * @param bool           $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
7876
     *                                                "mb_substr"</p>
7877
     *
7878
     * @psalm-pure
7879
     *
7880
     * @return string[][]
7881
     *                    <p>An array containing chunks of the input.</p>
7882
     */
7883 1
    public static function str_split_array(
7884
        array $input,
7885
        int $length = 1,
7886
        bool $clean_utf8 = false,
7887
        bool $try_to_use_mb_functions = true
7888
    ): array {
7889 1
        foreach ($input as $k => &$v) {
7890 1
            $v = self::str_split(
7891 1
                $v,
7892 1
                $length,
7893 1
                $clean_utf8,
7894 1
                $try_to_use_mb_functions
7895
            );
7896
        }
7897
7898
        /** @var string[][] $input */
7899 1
        return $input;
7900
    }
7901
7902
    /**
7903
     * Convert a string to an array of unicode characters.
7904
     *
7905
     * @param int|string $input                   <p>The string or int to split into array.</p>
7906
     * @param int        $length                  [optional] <p>Max character length of each array
7907
     *                                            element.</p>
7908
     * @param bool       $clean_utf8              [optional] <p>Remove non UTF-8 chars from the
7909
     *                                            string.</p>
7910
     * @param bool       $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
7911
     *                                            "mb_substr"</p>
7912
     *
7913
     * @psalm-pure
7914
     *
7915
     * @return string[]
7916
     *                  <p>An array containing chunks of chars from the input.</p>
7917
     *
7918
     * @noinspection SuspiciousBinaryOperationInspection
7919
     */
7920 89
    public static function str_split(
7921
        $input,
7922
        int $length = 1,
7923
        bool $clean_utf8 = false,
7924
        bool $try_to_use_mb_functions = true
7925
    ): array {
7926 89
        if ($length <= 0) {
7927 3
            return [];
7928
        }
7929
7930
        // this is only an old fallback
7931
        /** @noinspection PhpSillyAssignmentInspection - hack for phpstan */
7932
        /** @var int|int[]|string|string[] $input */
7933 88
        $input = $input;
7934 88
        if (\is_array($input)) {
7935
            /**
7936
             * @psalm-suppress InvalidReturnStatement
7937
             */
7938
            return self::str_split_array(
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::str_split_a...ry_to_use_mb_functions) returns the type array<mixed,string[]> which is incompatible with the documented return type string[].
Loading history...
7939
                $input,
7940
                $length,
7941
                $clean_utf8,
7942
                $try_to_use_mb_functions
7943
            );
7944
        }
7945
7946
        // init
7947 88
        $input = (string) $input;
7948
7949 88
        if ($input === '') {
7950 13
            return [];
7951
        }
7952
7953 85
        if ($clean_utf8) {
7954 19
            $input = self::clean($input);
7955
        }
7956
7957
        if (
7958 85
            $try_to_use_mb_functions
7959
            &&
7960 85
            self::$SUPPORT['mbstring'] === true
7961
        ) {
7962 81
            if (Bootup::is_php('7.4')) {
7963
                /**
7964
                 * @psalm-suppress ImpureFunctionCall - why?
7965
                 */
7966
                $return = \mb_str_split($input, $length);
7967
                if ($return !== false) {
7968
                    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...
7969
                }
7970
            }
7971
7972 81
            $i_max = \mb_strlen($input);
7973 81
            if ($i_max <= 127) {
7974 75
                $ret = [];
7975 75
                for ($i = 0; $i < $i_max; ++$i) {
7976 75
                    $ret[] = \mb_substr($input, $i, 1);
7977
                }
7978
            } else {
7979 16
                $return_array = [];
7980 16
                \preg_match_all('/./us', $input, $return_array);
7981 81
                $ret = $return_array[0] ?? [];
7982
            }
7983 23
        } elseif (self::$SUPPORT['pcre_utf8'] === true) {
7984 17
            $return_array = [];
7985 17
            \preg_match_all('/./us', $input, $return_array);
7986 17
            $ret = $return_array[0] ?? [];
7987
        } else {
7988
7989
            // fallback
7990
7991 8
            $ret = [];
7992 8
            $len = \strlen($input);
7993
7994
            /** @noinspection ForeachInvariantsInspection */
7995 8
            for ($i = 0; $i < $len; ++$i) {
7996 8
                if (($input[$i] & "\x80") === "\x00") {
7997 8
                    $ret[] = $input[$i];
7998
                } elseif (
7999 8
                    isset($input[$i + 1])
8000
                    &&
8001 8
                    ($input[$i] & "\xE0") === "\xC0"
8002
                ) {
8003 4
                    if (($input[$i + 1] & "\xC0") === "\x80") {
8004 4
                        $ret[] = $input[$i] . $input[$i + 1];
8005
8006 4
                        ++$i;
8007
                    }
8008
                } elseif (
8009 6
                    isset($input[$i + 2])
8010
                    &&
8011 6
                    ($input[$i] & "\xF0") === "\xE0"
8012
                ) {
8013
                    if (
8014 6
                        ($input[$i + 1] & "\xC0") === "\x80"
8015
                        &&
8016 6
                        ($input[$i + 2] & "\xC0") === "\x80"
8017
                    ) {
8018 6
                        $ret[] = $input[$i] . $input[$i + 1] . $input[$i + 2];
8019
8020 6
                        $i += 2;
8021
                    }
8022
                } elseif (
8023
                    isset($input[$i + 3])
8024
                    &&
8025
                    ($input[$i] & "\xF8") === "\xF0"
8026
                ) {
8027
                    if (
8028
                        ($input[$i + 1] & "\xC0") === "\x80"
8029
                        &&
8030
                        ($input[$i + 2] & "\xC0") === "\x80"
8031
                        &&
8032
                        ($input[$i + 3] & "\xC0") === "\x80"
8033
                    ) {
8034
                        $ret[] = $input[$i] . $input[$i + 1] . $input[$i + 2] . $input[$i + 3];
8035
8036
                        $i += 3;
8037
                    }
8038
                }
8039
            }
8040
        }
8041
8042 85
        if ($length > 1) {
8043 11
            $ret = \array_chunk($ret, $length);
8044
8045 11
            return \array_map(
8046
                static function (array &$item): string {
8047 11
                    return \implode('', $item);
8048 11
                },
8049 11
                $ret
8050
            );
8051
        }
8052
8053 78
        if (isset($ret[0]) && $ret[0] === '') {
8054
            return [];
8055
        }
8056
8057 78
        return $ret;
8058
    }
8059
8060
    /**
8061
     * Splits the string with the provided regular expression, returning an
8062
     * array of strings. An optional integer $limit will truncate the
8063
     * results.
8064
     *
8065
     * @param string $str
8066
     * @param string $pattern <p>The regex with which to split the string.</p>
8067
     * @param int    $limit   [optional] <p>Maximum number of results to return. Default: -1 === no limit</p>
8068
     *
8069
     * @psalm-pure
8070
     *
8071
     * @return string[]
8072
     *                  <p>An array of strings.</p>
8073
     */
8074 16
    public static function str_split_pattern(string $str, string $pattern, int $limit = -1): array
8075
    {
8076 16
        if ($limit === 0) {
8077 2
            return [];
8078
        }
8079
8080 14
        if ($pattern === '') {
8081 1
            return [$str];
8082
        }
8083
8084 13
        if (self::$SUPPORT['mbstring'] === true) {
8085 13
            if ($limit >= 0) {
8086
                /** @noinspection PhpComposerExtensionStubsInspection */
8087 8
                $result_tmp = \mb_split($pattern, $str);
8088
8089 8
                $result = [];
8090 8
                foreach ($result_tmp as $item_tmp) {
8091 8
                    if ($limit === 0) {
8092 4
                        break;
8093
                    }
8094 8
                    --$limit;
8095
8096 8
                    $result[] = $item_tmp;
8097
                }
8098
8099 8
                return $result;
8100
            }
8101
8102
            /** @noinspection PhpComposerExtensionStubsInspection */
8103 5
            return \mb_split($pattern, $str);
8104
        }
8105
8106
        if ($limit > 0) {
8107
            ++$limit;
8108
        } else {
8109
            $limit = -1;
8110
        }
8111
8112
        $array = \preg_split('/' . \preg_quote($pattern, '/') . '/u', $str, $limit);
8113
8114
        if ($array === false) {
8115
            return [];
8116
        }
8117
8118
        if ($limit > 0 && \count($array) === $limit) {
8119
            \array_pop($array);
8120
        }
8121
8122
        return $array;
8123
    }
8124
8125
    /**
8126
     * Check if the string starts with the given substring.
8127
     *
8128
     * @param string $haystack <p>The string to search in.</p>
8129
     * @param string $needle   <p>The substring to search for.</p>
8130
     *
8131
     * @psalm-pure
8132
     *
8133
     * @return bool
8134
     */
8135 19
    public static function str_starts_with(string $haystack, string $needle): bool
8136
    {
8137 19
        if ($needle === '') {
8138 2
            return true;
8139
        }
8140
8141 19
        if ($haystack === '') {
8142
            return false;
8143
        }
8144
8145 19
        return \strpos($haystack, $needle) === 0;
8146
    }
8147
8148
    /**
8149
     * Returns true if the string begins with any of $substrings, false otherwise.
8150
     *
8151
     * - case-sensitive
8152
     *
8153
     * @param string $str        <p>The input string.</p>
8154
     * @param array  $substrings <p>Substrings to look for.</p>
8155
     *
8156
     * @psalm-pure
8157
     *
8158
     * @return bool whether or not $str starts with $substring
8159
     */
8160 8
    public static function str_starts_with_any(string $str, array $substrings): bool
8161
    {
8162 8
        if ($str === '') {
8163
            return false;
8164
        }
8165
8166 8
        if ($substrings === []) {
8167
            return false;
8168
        }
8169
8170 8
        foreach ($substrings as &$substring) {
8171 8
            if (self::str_starts_with($str, $substring)) {
8172 8
                return true;
8173
            }
8174
        }
8175
8176 6
        return false;
8177
    }
8178
8179
    /**
8180
     * Gets the substring after the first occurrence of a separator.
8181
     *
8182
     * @param string $str       <p>The input string.</p>
8183
     * @param string $separator <p>The string separator.</p>
8184
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8185
     *
8186
     * @psalm-pure
8187
     *
8188
     * @return string
8189
     */
8190 1
    public static function str_substr_after_first_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
8191
    {
8192 1
        if ($separator === '' || $str === '') {
8193 1
            return '';
8194
        }
8195
8196 1
        if ($encoding === 'UTF-8') {
8197 1
            $offset = \mb_strpos($str, $separator);
8198 1
            if ($offset === false) {
8199 1
                return '';
8200
            }
8201
8202 1
            return (string) \mb_substr(
8203 1
                $str,
8204 1
                $offset + (int) \mb_strlen($separator)
8205
            );
8206
        }
8207
8208
        $offset = self::strpos($str, $separator, 0, $encoding);
8209
        if ($offset === false) {
8210
            return '';
8211
        }
8212
8213
        return (string) \mb_substr(
8214
            $str,
8215
            $offset + (int) self::strlen($separator, $encoding),
8216
            null,
8217
            $encoding
8218
        );
8219
    }
8220
8221
    /**
8222
     * Gets the substring after the last occurrence of a separator.
8223
     *
8224
     * @param string $str       <p>The input string.</p>
8225
     * @param string $separator <p>The string separator.</p>
8226
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8227
     *
8228
     * @psalm-pure
8229
     *
8230
     * @return string
8231
     */
8232 1
    public static function str_substr_after_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
8233
    {
8234 1
        if ($separator === '' || $str === '') {
8235 1
            return '';
8236
        }
8237
8238 1
        if ($encoding === 'UTF-8') {
8239 1
            $offset = \mb_strrpos($str, $separator);
8240 1
            if ($offset === false) {
8241 1
                return '';
8242
            }
8243
8244 1
            return (string) \mb_substr(
8245 1
                $str,
8246 1
                $offset + (int) \mb_strlen($separator)
8247
            );
8248
        }
8249
8250
        $offset = self::strrpos($str, $separator, 0, $encoding);
8251
        if ($offset === false) {
8252
            return '';
8253
        }
8254
8255
        return (string) self::substr(
8256
            $str,
8257
            $offset + (int) self::strlen($separator, $encoding),
8258
            null,
8259
            $encoding
8260
        );
8261
    }
8262
8263
    /**
8264
     * Gets the substring before the first occurrence of a separator.
8265
     *
8266
     * @param string $str       <p>The input string.</p>
8267
     * @param string $separator <p>The string separator.</p>
8268
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8269
     *
8270
     * @psalm-pure
8271
     *
8272
     * @return string
8273
     */
8274 1
    public static function str_substr_before_first_separator(
8275
        string $str,
8276
        string $separator,
8277
        string $encoding = 'UTF-8'
8278
    ): string {
8279 1
        if ($separator === '' || $str === '') {
8280 1
            return '';
8281
        }
8282
8283 1
        if ($encoding === 'UTF-8') {
8284 1
            $offset = \mb_strpos($str, $separator);
8285 1
            if ($offset === false) {
8286 1
                return '';
8287
            }
8288
8289 1
            return (string) \mb_substr(
8290 1
                $str,
8291 1
                0,
8292 1
                $offset
8293
            );
8294
        }
8295
8296
        $offset = self::strpos($str, $separator, 0, $encoding);
8297
        if ($offset === false) {
8298
            return '';
8299
        }
8300
8301
        return (string) self::substr(
8302
            $str,
8303
            0,
8304
            $offset,
8305
            $encoding
8306
        );
8307
    }
8308
8309
    /**
8310
     * Gets the substring before the last occurrence of a separator.
8311
     *
8312
     * @param string $str       <p>The input string.</p>
8313
     * @param string $separator <p>The string separator.</p>
8314
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8315
     *
8316
     * @psalm-pure
8317
     *
8318
     * @return string
8319
     */
8320 1
    public static function str_substr_before_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
8321
    {
8322 1
        if ($separator === '' || $str === '') {
8323 1
            return '';
8324
        }
8325
8326 1
        if ($encoding === 'UTF-8') {
8327 1
            $offset = \mb_strrpos($str, $separator);
8328 1
            if ($offset === false) {
8329 1
                return '';
8330
            }
8331
8332 1
            return (string) \mb_substr(
8333 1
                $str,
8334 1
                0,
8335 1
                $offset
8336
            );
8337
        }
8338
8339
        $offset = self::strrpos($str, $separator, 0, $encoding);
8340
        if ($offset === false) {
8341
            return '';
8342
        }
8343
8344
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8345
8346
        return (string) self::substr(
8347
            $str,
8348
            0,
8349
            $offset,
8350
            $encoding
8351
        );
8352
    }
8353
8354
    /**
8355
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
8356
     *
8357
     * @param string $str           <p>The input string.</p>
8358
     * @param string $needle        <p>The string to look for.</p>
8359
     * @param bool   $before_needle [optional] <p>Default: false</p>
8360
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
8361
     *
8362
     * @psalm-pure
8363
     *
8364
     * @return string
8365
     */
8366 2
    public static function str_substr_first(
8367
        string $str,
8368
        string $needle,
8369
        bool $before_needle = false,
8370
        string $encoding = 'UTF-8'
8371
    ): string {
8372 2
        if ($str === '' || $needle === '') {
8373 2
            return '';
8374
        }
8375
8376 2
        if ($encoding === 'UTF-8') {
8377 2
            if ($before_needle) {
8378 1
                $part = \mb_strstr(
8379 1
                    $str,
8380 1
                    $needle,
8381 1
                    $before_needle
8382
                );
8383
            } else {
8384 1
                $part = \mb_strstr(
8385 1
                    $str,
8386 2
                    $needle
8387
                );
8388
            }
8389
        } else {
8390
            $part = self::strstr(
8391
                $str,
8392
                $needle,
8393
                $before_needle,
8394
                $encoding
8395
            );
8396
        }
8397
8398 2
        return $part === false ? '' : $part;
8399
    }
8400
8401
    /**
8402
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
8403
     *
8404
     * @param string $str           <p>The input string.</p>
8405
     * @param string $needle        <p>The string to look for.</p>
8406
     * @param bool   $before_needle [optional] <p>Default: false</p>
8407
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
8408
     *
8409
     * @psalm-pure
8410
     *
8411
     * @return string
8412
     */
8413 2
    public static function str_substr_last(
8414
        string $str,
8415
        string $needle,
8416
        bool $before_needle = false,
8417
        string $encoding = 'UTF-8'
8418
    ): string {
8419 2
        if ($str === '' || $needle === '') {
8420 2
            return '';
8421
        }
8422
8423 2
        if ($encoding === 'UTF-8') {
8424 2
            if ($before_needle) {
8425 1
                $part = \mb_strrchr(
8426 1
                    $str,
8427 1
                    $needle,
8428 1
                    $before_needle
8429
                );
8430
            } else {
8431 1
                $part = \mb_strrchr(
8432 1
                    $str,
8433 2
                    $needle
8434
                );
8435
            }
8436
        } else {
8437
            $part = self::strrchr(
8438
                $str,
8439
                $needle,
8440
                $before_needle,
8441
                $encoding
8442
            );
8443
        }
8444
8445 2
        return $part === false ? '' : $part;
8446
    }
8447
8448
    /**
8449
     * Surrounds $str with the given substring.
8450
     *
8451
     * @param string $str
8452
     * @param string $substring <p>The substring to add to both sides.</p>
8453
     *
8454
     * @psalm-pure
8455
     *
8456
     * @return string
8457
     *                <p>A string with the substring both prepended and appended.</p>
8458
     */
8459 5
    public static function str_surround(string $str, string $substring): string
8460
    {
8461 5
        return $substring . $str . $substring;
8462
    }
8463
8464
    /**
8465
     * Returns a trimmed string with the first letter of each word capitalized.
8466
     * Also accepts an array, $ignore, allowing you to list words not to be
8467
     * capitalized.
8468
     *
8469
     * @param string              $str
8470
     * @param array|string[]|null $ignore                        [optional] <p>An array of words not to capitalize or
8471
     *                                                           null. Default: null</p>
8472
     * @param string              $encoding                      [optional] <p>Default: 'UTF-8'</p>
8473
     * @param bool                $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the
8474
     *                                                           string.</p>
8475
     * @param string|null         $lang                          [optional] <p>Set the language for special cases: az,
8476
     *                                                           el, lt, tr</p>
8477
     * @param bool                $try_to_keep_the_string_length [optional] <p>true === try to keep the string length:
8478
     *                                                           e.g. ẞ -> ß</p>
8479
     * @param bool                $use_trim_first                [optional] <p>true === trim the input string,
8480
     *                                                           first</p>
8481
     * @param string|null         $word_define_chars             [optional] <p>An string of chars that will be used as
8482
     *                                                           whitespace separator === words.</p>
8483
     *
8484
     * @psalm-pure
8485
     *
8486
     * @return string
8487
     *                <p>The titleized string.</p>
8488
     *
8489
     * @noinspection PhpTooManyParametersInspection
8490
     */
8491 10
    public static function str_titleize(
8492
        string $str,
8493
        array $ignore = null,
8494
        string $encoding = 'UTF-8',
8495
        bool $clean_utf8 = false,
8496
        string $lang = null,
8497
        bool $try_to_keep_the_string_length = false,
8498
        bool $use_trim_first = true,
8499
        string $word_define_chars = null
8500
    ): string {
8501 10
        if ($str === '') {
8502
            return '';
8503
        }
8504
8505 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
8506 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8507
        }
8508
8509 10
        if ($use_trim_first) {
8510 10
            $str = \trim($str);
8511
        }
8512
8513 10
        if ($clean_utf8) {
8514
            $str = self::clean($str);
8515
        }
8516
8517 10
        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;
8518
8519 10
        if ($word_define_chars) {
8520 4
            $word_define_chars = \preg_quote($word_define_chars, '/');
8521
        } else {
8522 6
            $word_define_chars = '';
8523
        }
8524
8525 10
        $str = (string) \preg_replace_callback(
8526 10
            '/([^\\s' . $word_define_chars . ']+)/u',
8527
            static function (array $match) use ($try_to_keep_the_string_length, $lang, $ignore, $use_mb_functions, $encoding): string {
8528 10
                if ($ignore !== null && \in_array($match[0], $ignore, true)) {
8529 4
                    return $match[0];
8530
                }
8531
8532 10
                if ($use_mb_functions) {
8533 10
                    if ($encoding === 'UTF-8') {
8534 10
                        return \mb_strtoupper(\mb_substr($match[0], 0, 1))
8535 10
                               . \mb_strtolower(\mb_substr($match[0], 1));
8536
                    }
8537
8538
                    return \mb_strtoupper(\mb_substr($match[0], 0, 1, $encoding), $encoding)
8539
                           . \mb_strtolower(\mb_substr($match[0], 1, null, $encoding), $encoding);
8540
                }
8541
8542
                return self::ucfirst(
8543
                    self::strtolower(
8544
                        $match[0],
8545
                        $encoding,
8546
                        false,
8547
                        $lang,
8548
                        $try_to_keep_the_string_length
8549
                    ),
8550
                    $encoding,
8551
                    false,
8552
                    $lang,
8553
                    $try_to_keep_the_string_length
8554
                );
8555 10
            },
8556 10
            $str
8557
        );
8558
8559 10
        return $str;
8560
    }
8561
8562
    /**
8563
     * Returns a trimmed string in proper title case.
8564
     *
8565
     * Also accepts an array, $ignore, allowing you to list words not to be
8566
     * capitalized.
8567
     *
8568
     * Adapted from John Gruber's script.
8569
     *
8570
     * @see https://gist.github.com/gruber/9f9e8650d68b13ce4d78
8571
     *
8572
     * @param string $str
8573
     * @param array  $ignore   <p>An array of words not to capitalize.</p>
8574
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8575
     *
8576
     * @psalm-pure
8577
     *
8578
     * @return string
8579
     *                <p>The titleized string.</p>
8580
     */
8581 35
    public static function str_titleize_for_humans(
8582
        string $str,
8583
        array $ignore = [],
8584
        string $encoding = 'UTF-8'
8585
    ): string {
8586 35
        if ($str === '') {
8587
            return '';
8588
        }
8589
8590
        $small_words = [
8591 35
            '(?<!q&)a',
8592
            'an',
8593
            'and',
8594
            'as',
8595
            'at(?!&t)',
8596
            'but',
8597
            'by',
8598
            'en',
8599
            'for',
8600
            'if',
8601
            'in',
8602
            'of',
8603
            'on',
8604
            'or',
8605
            'the',
8606
            'to',
8607
            'v[.]?',
8608
            'via',
8609
            'vs[.]?',
8610
        ];
8611
8612 35
        if ($ignore !== []) {
8613 1
            $small_words = \array_merge($small_words, $ignore);
8614
        }
8615
8616 35
        $small_words_rx = \implode('|', $small_words);
8617 35
        $apostrophe_rx = '(?x: [\'’] [[:lower:]]* )?';
8618
8619 35
        $str = \trim($str);
8620
8621 35
        if (!self::has_lowercase($str)) {
8622 2
            $str = self::strtolower($str, $encoding);
8623
        }
8624
8625
        // the main substitutions
8626
        /** @noinspection RegExpDuplicateAlternationBranch - false-positive - https://youtrack.jetbrains.com/issue/WI-51002 */
8627 35
        $str = (string) \preg_replace_callback(
8628
            '~\\b (_*) (?:                                                           # 1. Leading underscore and
8629
                        ( (?<=[ ][/\\\\]) [[:alpha:]]+ [-_[:alpha:]/\\\\]+ |                # 2. file path or 
8630 35
                          [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ' . $apostrophe_rx . ' )  #    URL, domain, or email
8631
                        |
8632 35
                        ( (?i: ' . $small_words_rx . ' ) ' . $apostrophe_rx . ' )           # 3. or small word (case-insensitive)
8633
                        |
8634 35
                        ( [[:alpha:]] [[:lower:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 4. or word w/o internal caps
8635
                        |
8636 35
                        ( [[:alpha:]] [[:alpha:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 5. or some other word
8637
                      ) (_*) \\b                                                          # 6. With trailing underscore
8638
                    ~ux',
8639
            /**
8640
             * @param string[] $matches
8641
             *
8642
             * @psalm-pure
8643
             *
8644
             * @return string
8645
             */
8646
            static function (array $matches) use ($encoding): string {
8647
                // preserve leading underscore
8648 35
                $str = $matches[1];
8649 35
                if ($matches[2]) {
8650
                    // preserve URLs, domains, emails and file paths
8651 5
                    $str .= $matches[2];
8652 35
                } elseif ($matches[3]) {
8653
                    // lower-case small words
8654 25
                    $str .= self::strtolower($matches[3], $encoding);
8655 35
                } elseif ($matches[4]) {
8656
                    // capitalize word w/o internal caps
8657 34
                    $str .= static::ucfirst($matches[4], $encoding);
8658
                } else {
8659
                    // preserve other kinds of word (iPhone)
8660 7
                    $str .= $matches[5];
8661
                }
8662
                // preserve trailing underscore
8663 35
                $str .= $matches[6];
8664
8665 35
                return $str;
8666 35
            },
8667 35
            $str
8668
        );
8669
8670
        // Exceptions for small words: capitalize at start of title...
8671 35
        $str = (string) \preg_replace_callback(
8672
            '~(  \\A [[:punct:]]*            # start of title...
8673
                      |  [:.;?!][ ]+                # or of subsentence...
8674
                      |  [ ][\'"“‘(\[][ ]* )        # or of inserted subphrase...
8675 35
                      ( ' . $small_words_rx . ' ) \\b # ...followed by small word
8676
                     ~uxi',
8677
            /**
8678
             * @param string[] $matches
8679
             *
8680
             * @psalm-pure
8681
             *
8682
             * @return string
8683
             */
8684
            static function (array $matches) use ($encoding): string {
8685 11
                return $matches[1] . static::ucfirst($matches[2], $encoding);
8686 35
            },
8687 35
            $str
8688
        );
8689
8690
        // ...and end of title
8691 35
        $str = (string) \preg_replace_callback(
8692 35
            '~\\b ( ' . $small_words_rx . ' ) # small word...
8693
                      (?= [[:punct:]]* \Z          # ...at the end of the title...
8694
                      |   [\'"’”)\]] [ ] )         # ...or of an inserted subphrase?
8695
                     ~uxi',
8696
            /**
8697
             * @param string[] $matches
8698
             *
8699
             * @psalm-pure
8700
             *
8701
             * @return string
8702
             */
8703
            static function (array $matches) use ($encoding): string {
8704 3
                return static::ucfirst($matches[1], $encoding);
8705 35
            },
8706 35
            $str
8707
        );
8708
8709
        // Exceptions for small words in hyphenated compound words.
8710
        // e.g. "in-flight" -> In-Flight
8711 35
        $str = (string) \preg_replace_callback(
8712
            '~\\b
8713
                        (?<! -)                   # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (in-flight)
8714 35
                        ( ' . $small_words_rx . ' )
8715
                        (?= -[[:alpha:]]+)        # lookahead for "-someword"
8716
                       ~uxi',
8717
            /**
8718
             * @param string[] $matches
8719
             *
8720
             * @psalm-pure
8721
             *
8722
             * @return string
8723
             */
8724
            static function (array $matches) use ($encoding): string {
8725
                return static::ucfirst($matches[1], $encoding);
8726 35
            },
8727 35
            $str
8728
        );
8729
8730
        // e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
8731 35
        $str = (string) \preg_replace_callback(
8732
            '~\\b
8733
                      (?<!…)                    # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (stand-in)
8734
                      ( [[:alpha:]]+- )         # $1 = first word and hyphen, should already be properly capped
8735 35
                      ( ' . $small_words_rx . ' ) # ...followed by small word
8736
                      (?!	- )                 # Negative lookahead for another -
8737
                     ~uxi',
8738
            /**
8739
             * @param string[] $matches
8740
             *
8741
             * @psalm-pure
8742
             *
8743
             * @return string
8744
             */
8745
            static function (array $matches) use ($encoding): string {
8746
                return $matches[1] . static::ucfirst($matches[2], $encoding);
8747 35
            },
8748 35
            $str
8749
        );
8750
8751 35
        return $str;
8752
    }
8753
8754
    /**
8755
     * Get a binary representation of a specific string.
8756
     *
8757
     * @param string $str <p>The input string.</p>
8758
     *
8759
     * @psalm-pure
8760
     *
8761
     * @return false|string
8762
     *                      <p>false on error</p>
8763
     */
8764 2
    public static function str_to_binary(string $str)
8765
    {
8766
        /** @var array|false $value - needed for PhpStan (stubs error) */
8767 2
        $value = \unpack('H*', $str);
8768 2
        if ($value === false) {
8769
            return false;
8770
        }
8771
8772
        /** @noinspection OffsetOperationsInspection */
8773 2
        return \base_convert($value[1], 16, 2);
8774
    }
8775
8776
    /**
8777
     * @param string   $str
8778
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
8779
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
8780
     *
8781
     * @psalm-pure
8782
     *
8783
     * @return string[]
8784
     */
8785 17
    public static function str_to_lines(string $str, bool $remove_empty_values = false, int $remove_short_values = null): array
8786
    {
8787 17
        if ($str === '') {
8788 1
            return $remove_empty_values ? [] : [''];
8789
        }
8790
8791 16
        if (self::$SUPPORT['mbstring'] === true) {
8792
            /** @noinspection PhpComposerExtensionStubsInspection */
8793 16
            $return = \mb_split("[\r\n]{1,2}", $str);
8794
        } else {
8795
            $return = \preg_split("/[\r\n]{1,2}/u", $str);
8796
        }
8797
8798 16
        if ($return === false) {
8799
            return $remove_empty_values ? [] : [''];
8800
        }
8801
8802
        if (
8803 16
            $remove_short_values === null
8804
            &&
8805 16
            !$remove_empty_values
8806
        ) {
8807 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...
8808
        }
8809
8810
        return self::reduce_string_array(
8811
            $return,
8812
            $remove_empty_values,
8813
            $remove_short_values
8814
        );
8815
    }
8816
8817
    /**
8818
     * Convert a string into an array of words.
8819
     *
8820
     * @param string   $str
8821
     * @param string   $char_list           <p>Additional chars for the definition of "words".</p>
8822
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
8823
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
8824
     *
8825
     * @psalm-pure
8826
     *
8827
     * @return string[]
8828
     */
8829 13
    public static function str_to_words(
8830
        string $str,
8831
        string $char_list = '',
8832
        bool $remove_empty_values = false,
8833
        int $remove_short_values = null
8834
    ): array {
8835 13
        if ($str === '') {
8836 4
            return $remove_empty_values ? [] : [''];
8837
        }
8838
8839 13
        $char_list = self::rxClass($char_list, '\pL');
8840
8841 13
        $return = \preg_split("/({$char_list}+(?:[\p{Pd}’']{$char_list}+)*)/u", $str, -1, \PREG_SPLIT_DELIM_CAPTURE);
8842 13
        if ($return === false) {
8843
            return $remove_empty_values ? [] : [''];
8844
        }
8845
8846
        if (
8847 13
            $remove_short_values === null
8848
            &&
8849 13
            !$remove_empty_values
8850
        ) {
8851 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...
8852
        }
8853
8854 2
        $tmp_return = self::reduce_string_array(
8855 2
            $return,
8856 2
            $remove_empty_values,
8857 2
            $remove_short_values
8858
        );
8859
8860 2
        foreach ($tmp_return as &$item) {
8861 2
            $item = (string) $item;
8862
        }
8863
8864 2
        return $tmp_return;
8865
    }
8866
8867
    /**
8868
     * alias for "UTF8::to_ascii()"
8869
     *
8870
     * @param string $str
8871
     * @param string $unknown
8872
     * @param bool   $strict
8873
     *
8874
     * @psalm-pure
8875
     *
8876
     * @return string
8877
     *
8878
     * @see        UTF8::to_ascii()
8879
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
8880
     */
8881 7
    public static function str_transliterate(
8882
        string $str,
8883
        string $unknown = '?',
8884
        bool $strict = false
8885
    ): string {
8886 7
        return self::to_ascii($str, $unknown, $strict);
8887
    }
8888
8889
    /**
8890
     * Truncates the string to a given length. If $substring is provided, and
8891
     * truncating occurs, the string is further truncated so that the substring
8892
     * may be appended without exceeding the desired length.
8893
     *
8894
     * @param string $str
8895
     * @param int    $length    <p>Desired length of the truncated string.</p>
8896
     * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
8897
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8898
     *
8899
     * @psalm-pure
8900
     *
8901
     * @return string
8902
     *                <p>A string after truncating.</p>
8903
     */
8904 22
    public static function str_truncate(
8905
        string $str,
8906
        int $length,
8907
        string $substring = '',
8908
        string $encoding = 'UTF-8'
8909
    ): string {
8910 22
        if ($str === '') {
8911
            return '';
8912
        }
8913
8914 22
        if ($encoding === 'UTF-8') {
8915 10
            if ($length >= (int) \mb_strlen($str)) {
8916 2
                return $str;
8917
            }
8918
8919 8
            if ($substring !== '') {
8920 4
                $length -= (int) \mb_strlen($substring);
8921
8922
                /** @noinspection UnnecessaryCastingInspection */
8923 4
                return (string) \mb_substr($str, 0, $length) . $substring;
8924
            }
8925
8926
            /** @noinspection UnnecessaryCastingInspection */
8927 4
            return (string) \mb_substr($str, 0, $length);
8928
        }
8929
8930 12
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8931
8932 12
        if ($length >= (int) self::strlen($str, $encoding)) {
8933 2
            return $str;
8934
        }
8935
8936 10
        if ($substring !== '') {
8937 6
            $length -= (int) self::strlen($substring, $encoding);
8938
        }
8939
8940
        return (
8941 10
               (string) self::substr(
8942 10
                   $str,
8943 10
                   0,
8944 10
                   $length,
8945 10
                   $encoding
8946
               )
8947 10
               ) . $substring;
8948
    }
8949
8950
    /**
8951
     * Truncates the string to a given length, while ensuring that it does not
8952
     * split words. If $substring is provided, and truncating occurs, the
8953
     * string is further truncated so that the substring may be appended without
8954
     * exceeding the desired length.
8955
     *
8956
     * @param string $str
8957
     * @param int    $length                                 <p>Desired length of the truncated string.</p>
8958
     * @param string $substring                              [optional] <p>The substring to append if it can fit.
8959
     *                                                       Default:
8960
     *                                                       ''</p>
8961
     * @param string $encoding                               [optional] <p>Default: 'UTF-8'</p>
8962
     * @param bool   $ignore_do_not_split_words_for_one_word [optional] <p>Default: false</p>
8963
     *
8964
     * @psalm-pure
8965
     *
8966
     * @return string
8967
     *                <p>A string after truncating.</p>
8968
     */
8969 47
    public static function str_truncate_safe(
8970
        string $str,
8971
        int $length,
8972
        string $substring = '',
8973
        string $encoding = 'UTF-8',
8974
        bool $ignore_do_not_split_words_for_one_word = false
8975
    ): string {
8976 47
        if ($str === '' || $length <= 0) {
8977 1
            return $substring;
8978
        }
8979
8980 47
        if ($encoding === 'UTF-8') {
8981 21
            if ($length >= (int) \mb_strlen($str)) {
8982 5
                return $str;
8983
            }
8984
8985
            // need to further trim the string so we can append the substring
8986 17
            $length -= (int) \mb_strlen($substring);
8987 17
            if ($length <= 0) {
8988 1
                return $substring;
8989
            }
8990
8991
            /** @var false|string $truncated - needed for PhpStan (stubs error) */
8992 17
            $truncated = \mb_substr($str, 0, $length);
8993 17
            if ($truncated === false) {
8994
                return '';
8995
            }
8996
8997
            // if the last word was truncated
8998 17
            $space_position = \mb_strpos($str, ' ', $length - 1);
8999 17
            if ($space_position !== $length) {
9000
                // find pos of the last occurrence of a space, get up to that
9001 13
                $last_position = \mb_strrpos($truncated, ' ', 0);
9002
9003
                if (
9004 13
                    $last_position !== false
9005
                    ||
9006
                    (
9007 3
                        $space_position !== false
9008
                        &&
9009 13
                         !$ignore_do_not_split_words_for_one_word
9010
                    )
9011
                ) {
9012 17
                    $truncated = (string) \mb_substr($truncated, 0, (int) $last_position);
9013
                }
9014
            }
9015
        } else {
9016 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9017
9018 26
            if ($length >= (int) self::strlen($str, $encoding)) {
9019 4
                return $str;
9020
            }
9021
9022
            // need to further trim the string so we can append the substring
9023 22
            $length -= (int) self::strlen($substring, $encoding);
9024 22
            if ($length <= 0) {
9025
                return $substring;
9026
            }
9027
9028 22
            $truncated = self::substr($str, 0, $length, $encoding);
9029
9030 22
            if ($truncated === false) {
9031
                return '';
9032
            }
9033
9034
            // if the last word was truncated
9035 22
            $space_position = self::strpos($str, ' ', $length - 1, $encoding);
9036 22
            if ($space_position !== $length) {
9037
                // find pos of the last occurrence of a space, get up to that
9038 12
                $last_position = self::strrpos($truncated, ' ', 0, $encoding);
9039
9040
                if (
9041 12
                    $last_position !== false
9042
                    ||
9043
                    (
9044 4
                        $space_position !== false
9045
                        &&
9046 12
                        !$ignore_do_not_split_words_for_one_word
9047
                    )
9048
                ) {
9049 9
                    $truncated = (string) self::substr($truncated, 0, (int) $last_position, $encoding);
9050
                }
9051
            }
9052
        }
9053
9054 39
        return $truncated . $substring;
9055
    }
9056
9057
    /**
9058
     * Returns a lowercase and trimmed string separated by underscores.
9059
     * Underscores are inserted before uppercase characters (with the exception
9060
     * of the first character of the string), and in place of spaces as well as
9061
     * dashes.
9062
     *
9063
     * @param string $str
9064
     *
9065
     * @psalm-pure
9066
     *
9067
     * @return string
9068
     *                <p>The underscored string.</p>
9069
     */
9070 16
    public static function str_underscored(string $str): string
9071
    {
9072 16
        return self::str_delimit($str, '_');
9073
    }
9074
9075
    /**
9076
     * Returns an UpperCamelCase version of the supplied string. It trims
9077
     * surrounding spaces, capitalizes letters following digits, spaces, dashes
9078
     * and underscores, and removes spaces, dashes, underscores.
9079
     *
9080
     * @param string      $str                           <p>The input string.</p>
9081
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
9082
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
9083
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
9084
     *                                                   tr</p>
9085
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
9086
     *                                                   -> ß</p>
9087
     *
9088
     * @psalm-pure
9089
     *
9090
     * @return string
9091
     *                <p>A string in UpperCamelCase.</p>
9092
     */
9093 13
    public static function str_upper_camelize(
9094
        string $str,
9095
        string $encoding = 'UTF-8',
9096
        bool $clean_utf8 = false,
9097
        string $lang = null,
9098
        bool $try_to_keep_the_string_length = false
9099
    ): string {
9100 13
        return self::ucfirst(self::str_camelize($str, $encoding), $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
9101
    }
9102
9103
    /**
9104
     * alias for "UTF8::ucfirst()"
9105
     *
9106
     * @param string      $str
9107
     * @param string      $encoding
9108
     * @param bool        $clean_utf8
9109
     * @param string|null $lang
9110
     * @param bool        $try_to_keep_the_string_length
9111
     *
9112
     * @psalm-pure
9113
     *
9114
     * @return string
9115
     *
9116
     * @see        UTF8::ucfirst()
9117
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
9118
     */
9119 5
    public static function str_upper_first(
9120
        string $str,
9121
        string $encoding = 'UTF-8',
9122
        bool $clean_utf8 = false,
9123
        string $lang = null,
9124
        bool $try_to_keep_the_string_length = false
9125
    ): string {
9126 5
        return self::ucfirst(
9127 5
            $str,
9128 5
            $encoding,
9129 5
            $clean_utf8,
9130 5
            $lang,
9131 5
            $try_to_keep_the_string_length
9132
        );
9133
    }
9134
9135
    /**
9136
     * Get the number of words in a specific string.
9137
     *
9138
     * @param string $str       <p>The input string.</p>
9139
     * @param int    $format    [optional] <p>
9140
     *                          <strong>0</strong> => return a number of words (default)<br>
9141
     *                          <strong>1</strong> => return an array of words<br>
9142
     *                          <strong>2</strong> => return an array of words with word-offset as key
9143
     *                          </p>
9144
     * @param string $char_list [optional] <p>Additional chars that contains to words and do not start a new word.</p>
9145
     *
9146
     * @psalm-pure
9147
     *
9148
     * @return int|string[]
9149
     *                      <p>The number of words in the string.</p>
9150
     */
9151 2
    public static function str_word_count(string $str, int $format = 0, string $char_list = '')
9152
    {
9153 2
        $str_parts = self::str_to_words($str, $char_list);
9154
9155 2
        $len = \count($str_parts);
9156
9157 2
        if ($format === 1) {
9158 2
            $number_of_words = [];
9159 2
            for ($i = 1; $i < $len; $i += 2) {
9160 2
                $number_of_words[] = $str_parts[$i];
9161
            }
9162 2
        } elseif ($format === 2) {
9163 2
            $number_of_words = [];
9164 2
            $offset = (int) self::strlen($str_parts[0]);
9165 2
            for ($i = 1; $i < $len; $i += 2) {
9166 2
                $number_of_words[$offset] = $str_parts[$i];
9167 2
                $offset += (int) self::strlen($str_parts[$i]) + (int) self::strlen($str_parts[$i + 1]);
9168
            }
9169
        } else {
9170 2
            $number_of_words = (int) (($len - 1) / 2);
9171
        }
9172
9173 2
        return $number_of_words;
9174
    }
9175
9176
    /**
9177
     * Case-insensitive string comparison.
9178
     *
9179
     * INFO: Case-insensitive version of UTF8::strcmp()
9180
     *
9181
     * @param string $str1     <p>The first string.</p>
9182
     * @param string $str2     <p>The second string.</p>
9183
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9184
     *
9185
     * @psalm-pure
9186
     *
9187
     * @return int
9188
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
9189
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
9190
     *             <strong>0</strong> if they are equal
9191
     */
9192 23
    public static function strcasecmp(
9193
        string $str1,
9194
        string $str2,
9195
        string $encoding = 'UTF-8'
9196
    ): int {
9197 23
        return self::strcmp(
9198 23
            self::strtocasefold(
9199 23
                $str1,
9200 23
                true,
9201 23
                false,
9202 23
                $encoding,
9203 23
                null,
9204 23
                false
9205
            ),
9206 23
            self::strtocasefold(
9207 23
                $str2,
9208 23
                true,
9209 23
                false,
9210 23
                $encoding,
9211 23
                null,
9212 23
                false
9213
            )
9214
        );
9215
    }
9216
9217
    /**
9218
     * alias for "UTF8::strstr()"
9219
     *
9220
     * @param string $haystack
9221
     * @param string $needle
9222
     * @param bool   $before_needle
9223
     * @param string $encoding
9224
     * @param bool   $clean_utf8
9225
     *
9226
     * @psalm-pure
9227
     *
9228
     * @return false|string
9229
     *
9230
     * @see        UTF8::strstr()
9231
     * @deprecated <p>please use "UTF8::strstr()"</p>
9232
     */
9233 2
    public static function strchr(
9234
        string $haystack,
9235
        string $needle,
9236
        bool $before_needle = false,
9237
        string $encoding = 'UTF-8',
9238
        bool $clean_utf8 = false
9239
    ) {
9240 2
        return self::strstr(
9241 2
            $haystack,
9242 2
            $needle,
9243 2
            $before_needle,
9244 2
            $encoding,
9245 2
            $clean_utf8
9246
        );
9247
    }
9248
9249
    /**
9250
     * Case-sensitive string comparison.
9251
     *
9252
     * @param string $str1 <p>The first string.</p>
9253
     * @param string $str2 <p>The second string.</p>
9254
     *
9255
     * @psalm-pure
9256
     *
9257
     * @return int
9258
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
9259
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
9260
     *             <strong>0</strong> if they are equal
9261
     */
9262 29
    public static function strcmp(string $str1, string $str2): int
9263
    {
9264 29
        if ($str1 === $str2) {
9265 21
            return 0;
9266
        }
9267
9268 24
        return \strcmp(
9269 24
            \Normalizer::normalize($str1, \Normalizer::NFD),
9270 24
            \Normalizer::normalize($str2, \Normalizer::NFD)
9271
        );
9272
    }
9273
9274
    /**
9275
     * Find length of initial segment not matching mask.
9276
     *
9277
     * @param string $str
9278
     * @param string $char_list
9279
     * @param int    $offset
9280
     * @param int    $length
9281
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
9282
     *
9283
     * @psalm-pure
9284
     *
9285
     * @return int
9286
     */
9287 12
    public static function strcspn(
9288
        string $str,
9289
        string $char_list,
9290
        int $offset = null,
9291
        int $length = null,
9292
        string $encoding = 'UTF-8'
9293
    ): int {
9294 12
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9295
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9296
        }
9297
9298 12
        if ($char_list === '') {
9299 2
            return (int) self::strlen($str, $encoding);
9300
        }
9301
9302 11
        if ($offset !== null || $length !== null) {
9303 3
            if ($encoding === 'UTF-8') {
9304 3
                if ($length === null) {
9305
                    /** @noinspection UnnecessaryCastingInspection */
9306 2
                    $str_tmp = \mb_substr($str, (int) $offset);
9307
                } else {
9308
                    /** @noinspection UnnecessaryCastingInspection */
9309 3
                    $str_tmp = \mb_substr($str, (int) $offset, $length);
9310
                }
9311
            } else {
9312
                /** @noinspection UnnecessaryCastingInspection */
9313
                $str_tmp = self::substr($str, (int) $offset, $length, $encoding);
9314
            }
9315
9316 3
            if ($str_tmp === false) {
9317
                return 0;
9318
            }
9319
9320
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
9321 3
            $str = $str_tmp;
9322
        }
9323
9324 11
        if ($str === '') {
9325 2
            return 0;
9326
        }
9327
9328 10
        $matches = [];
9329 10
        if (\preg_match('/^(.*?)' . self::rxClass($char_list) . '/us', $str, $matches)) {
9330 9
            $return = self::strlen($matches[1], $encoding);
9331 9
            if ($return === false) {
9332
                return 0;
9333
            }
9334
9335 9
            return $return;
9336
        }
9337
9338 2
        return (int) self::strlen($str, $encoding);
9339
    }
9340
9341
    /**
9342
     * alias for "UTF8::stristr()"
9343
     *
9344
     * @param string $haystack
9345
     * @param string $needle
9346
     * @param bool   $before_needle
9347
     * @param string $encoding
9348
     * @param bool   $clean_utf8
9349
     *
9350
     * @psalm-pure
9351
     *
9352
     * @return false|string
9353
     *
9354
     * @see        UTF8::stristr()
9355
     * @deprecated <p>please use "UTF8::stristr()"</p>
9356
     */
9357 1
    public static function strichr(
9358
        string $haystack,
9359
        string $needle,
9360
        bool $before_needle = false,
9361
        string $encoding = 'UTF-8',
9362
        bool $clean_utf8 = false
9363
    ) {
9364 1
        return self::stristr(
9365 1
            $haystack,
9366 1
            $needle,
9367 1
            $before_needle,
9368 1
            $encoding,
9369 1
            $clean_utf8
9370
        );
9371
    }
9372
9373
    /**
9374
     * Create a UTF-8 string from code points.
9375
     *
9376
     * INFO: opposite to UTF8::codepoints()
9377
     *
9378
     * @param array $array <p>Integer or Hexadecimal codepoints.</p>
9379
     *
9380
     * @psalm-pure
9381
     *
9382
     * @return string
9383
     *                <p>A UTF-8 encoded string.</p>
9384
     */
9385 4
    public static function string(array $array): string
9386
    {
9387 4
        if ($array === []) {
9388 4
            return '';
9389
        }
9390
9391 4
        $str = '';
9392 4
        foreach ($array as $strPart) {
9393 4
            $str .= '&#' . (int) $strPart . ';';
9394
        }
9395
9396 4
        return self::html_entity_decode($str, \ENT_QUOTES | \ENT_HTML5);
9397
    }
9398
9399
    /**
9400
     * Checks if string starts with "BOM" (Byte Order Mark Character) character.
9401
     *
9402
     * @param string $str <p>The input string.</p>
9403
     *
9404
     * @psalm-pure
9405
     *
9406
     * @return bool
9407
     *              <strong>true</strong> if the string has BOM at the start,<br>
9408
     *              <strong>false</strong> otherwise
9409
     */
9410 6
    public static function string_has_bom(string $str): bool
9411
    {
9412
        /** @noinspection PhpUnusedLocalVariableInspection */
9413 6
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
9414 6
            if (\strpos($str, $bom_string) === 0) {
9415 6
                return true;
9416
            }
9417
        }
9418
9419 6
        return false;
9420
    }
9421
9422
    /**
9423
     * Strip HTML and PHP tags from a string + clean invalid UTF-8.
9424
     *
9425
     * @see http://php.net/manual/en/function.strip-tags.php
9426
     *
9427
     * @param string $str            <p>
9428
     *                               The input string.
9429
     *                               </p>
9430
     * @param string $allowable_tags [optional] <p>
9431
     *                               You can use the optional second parameter to specify tags which should
9432
     *                               not be stripped.
9433
     *                               </p>
9434
     *                               <p>
9435
     *                               HTML comments and PHP tags are also stripped. This is hardcoded and
9436
     *                               can not be changed with allowable_tags.
9437
     *                               </p>
9438
     * @param bool   $clean_utf8     [optional] <p>Remove non UTF-8 chars from the string.</p>
9439
     *
9440
     * @psalm-pure
9441
     *
9442
     * @return string
9443
     *                <p>The stripped string.</p>
9444
     */
9445 4
    public static function strip_tags(
9446
        string $str,
9447
        string $allowable_tags = null,
9448
        bool $clean_utf8 = false
9449
    ): string {
9450 4
        if ($str === '') {
9451 1
            return '';
9452
        }
9453
9454 4
        if ($clean_utf8) {
9455 2
            $str = self::clean($str);
9456
        }
9457
9458 4
        if ($allowable_tags === null) {
9459 4
            return \strip_tags($str);
9460
        }
9461
9462 2
        return \strip_tags($str, $allowable_tags);
9463
    }
9464
9465
    /**
9466
     * Strip all whitespace characters. This includes tabs and newline
9467
     * characters, as well as multibyte whitespace such as the thin space
9468
     * and ideographic space.
9469
     *
9470
     * @param string $str
9471
     *
9472
     * @psalm-pure
9473
     *
9474
     * @return string
9475
     */
9476 36
    public static function strip_whitespace(string $str): string
9477
    {
9478 36
        if ($str === '') {
9479 3
            return '';
9480
        }
9481
9482 33
        return (string) \preg_replace('/[[:space:]]+/u', '', $str);
9483
    }
9484
9485
    /**
9486
     * Find the position of the first occurrence of a substring in a string, case-insensitive.
9487
     *
9488
     * @see http://php.net/manual/en/function.mb-stripos.php
9489
     *
9490
     * @param string $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
9491
     * @param string $needle     <p>The string to find in haystack.</p>
9492
     * @param int    $offset     [optional] <p>The position in haystack to start searching.</p>
9493
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9494
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9495
     *
9496
     * @psalm-pure
9497
     *
9498
     * @return false|int
9499
     *                   Return the <strong>(int)</strong> numeric position of the first occurrence of needle in the
9500
     *                   haystack string,<br> or <strong>false</strong> if needle is not found
9501
     */
9502 25
    public static function stripos(
9503
        string $haystack,
9504
        string $needle,
9505
        int $offset = 0,
9506
        string $encoding = 'UTF-8',
9507
        bool $clean_utf8 = false
9508
    ) {
9509 25
        if ($haystack === '' || $needle === '') {
9510 5
            return false;
9511
        }
9512
9513 24
        if ($clean_utf8) {
9514
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9515
            // if invalid characters are found in $haystack before $needle
9516 1
            $haystack = self::clean($haystack);
9517 1
            $needle = self::clean($needle);
9518
        }
9519
9520 24
        if (self::$SUPPORT['mbstring'] === true) {
9521 24
            if ($encoding === 'UTF-8') {
9522 24
                return \mb_stripos($haystack, $needle, $offset);
9523
            }
9524
9525 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9526
9527 2
            return \mb_stripos($haystack, $needle, $offset, $encoding);
9528
        }
9529
9530 2
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
9531
9532
        if (
9533 2
            $encoding === 'UTF-8' // INFO: "grapheme_stripos()" can't handle other encodings
9534
            &&
9535 2
            $offset >= 0 // grapheme_stripos() can't handle negative offset
9536
            &&
9537 2
            self::$SUPPORT['intl'] === true
9538
        ) {
9539
            $return_tmp = \grapheme_stripos($haystack, $needle, $offset);
9540
            if ($return_tmp !== false) {
9541
                return $return_tmp;
9542
            }
9543
        }
9544
9545
        //
9546
        // fallback for ascii only
9547
        //
9548
9549 2
        if (ASCII::is_ascii($haystack . $needle)) {
9550
            return \stripos($haystack, $needle, $offset);
9551
        }
9552
9553
        //
9554
        // fallback via vanilla php
9555
        //
9556
9557 2
        $haystack = self::strtocasefold($haystack, true, false, $encoding, null, false);
9558 2
        $needle = self::strtocasefold($needle, true, false, $encoding, null, false);
9559
9560 2
        return self::strpos($haystack, $needle, $offset, $encoding);
9561
    }
9562
9563
    /**
9564
     * Returns all of haystack starting from and including the first occurrence of needle to the end.
9565
     *
9566
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
9567
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
9568
     * @param bool   $before_needle [optional] <p>
9569
     *                              If <b>TRUE</b>, it returns the part of the
9570
     *                              haystack before the first occurrence of the needle (excluding the needle).
9571
     *                              </p>
9572
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9573
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9574
     *
9575
     * @psalm-pure
9576
     *
9577
     * @return false|string
9578
     *                      <p>A sub-string,<br>or <strong>false</strong> if needle is not found.</p>
9579
     */
9580 12
    public static function stristr(
9581
        string $haystack,
9582
        string $needle,
9583
        bool $before_needle = false,
9584
        string $encoding = 'UTF-8',
9585
        bool $clean_utf8 = false
9586
    ) {
9587 12
        if ($haystack === '' || $needle === '') {
9588 3
            return false;
9589
        }
9590
9591 9
        if ($clean_utf8) {
9592
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9593
            // if invalid characters are found in $haystack before $needle
9594 1
            $needle = self::clean($needle);
9595 1
            $haystack = self::clean($haystack);
9596
        }
9597
9598 9
        if (!$needle) {
9599
            return $haystack;
9600
        }
9601
9602 9
        if (self::$SUPPORT['mbstring'] === true) {
9603 9
            if ($encoding === 'UTF-8') {
9604 9
                return \mb_stristr($haystack, $needle, $before_needle);
9605
            }
9606
9607 1
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9608
9609 1
            return \mb_stristr($haystack, $needle, $before_needle, $encoding);
9610
        }
9611
9612
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
9613
9614
        if (
9615
            $encoding !== 'UTF-8'
9616
            &&
9617
            self::$SUPPORT['mbstring'] === false
9618
        ) {
9619
            /**
9620
             * @psalm-suppress ImpureFunctionCall - is is only a warning
9621
             */
9622
            \trigger_error('UTF8::stristr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9623
        }
9624
9625
        if (
9626
            $encoding === 'UTF-8' // INFO: "grapheme_stristr()" can't handle other encodings
9627
            &&
9628
            self::$SUPPORT['intl'] === true
9629
        ) {
9630
            $return_tmp = \grapheme_stristr($haystack, $needle, $before_needle);
9631
            if ($return_tmp !== false) {
9632
                return $return_tmp;
9633
            }
9634
        }
9635
9636
        if (ASCII::is_ascii($needle . $haystack)) {
9637
            return \stristr($haystack, $needle, $before_needle);
9638
        }
9639
9640
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/usi', $haystack, $match);
9641
9642
        if (!isset($match[1])) {
9643
            return false;
9644
        }
9645
9646
        if ($before_needle) {
9647
            return $match[1];
9648
        }
9649
9650
        return self::substr($haystack, (int) self::strlen($match[1], $encoding), null, $encoding);
9651
    }
9652
9653
    /**
9654
     * Get the string length, not the byte-length!
9655
     *
9656
     * @see http://php.net/manual/en/function.mb-strlen.php
9657
     *
9658
     * @param string $str        <p>The string being checked for length.</p>
9659
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9660
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9661
     *
9662
     * @psalm-pure
9663
     *
9664
     * @return false|int
9665
     *                   <p>
9666
     *                   The number <strong>(int)</strong> of characters in the string $str having character encoding
9667
     *                   $encoding.
9668
     *                   (One multi-byte character counted as +1).
9669
     *                   <br>
9670
     *                   Can return <strong>false</strong>, if e.g. mbstring is not installed and we process invalid
9671
     *                   chars.
9672
     *                   </p>
9673
     */
9674 174
    public static function strlen(
9675
        string $str,
9676
        string $encoding = 'UTF-8',
9677
        bool $clean_utf8 = false
9678
    ) {
9679 174
        if ($str === '') {
9680 21
            return 0;
9681
        }
9682
9683 172
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9684 12
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9685
        }
9686
9687 172
        if ($clean_utf8) {
9688
            // "mb_strlen" and "\iconv_strlen" returns wrong length,
9689
            // if invalid characters are found in $str
9690 4
            $str = self::clean($str);
9691
        }
9692
9693
        //
9694
        // fallback via mbstring
9695
        //
9696
9697 172
        if (self::$SUPPORT['mbstring'] === true) {
9698 166
            if ($encoding === 'UTF-8') {
9699
                /** @noinspection PhpUsageOfSilenceOperatorInspection - ignore warnings, it's working anyway */
9700 166
                return @\mb_strlen($str);
9701
            }
9702
9703
            /** @noinspection PhpUsageOfSilenceOperatorInspection - ignore warnings, it's working anyway */
9704 4
            return @\mb_strlen($str, $encoding);
9705
        }
9706
9707
        //
9708
        // fallback for binary || ascii only
9709
        //
9710
9711
        if (
9712 8
            $encoding === 'CP850'
9713
            ||
9714 8
            $encoding === 'ASCII'
9715
        ) {
9716
            return \strlen($str);
9717
        }
9718
9719
        if (
9720 8
            $encoding !== 'UTF-8'
9721
            &&
9722 8
            self::$SUPPORT['mbstring'] === false
9723
            &&
9724 8
            self::$SUPPORT['iconv'] === false
9725
        ) {
9726
            /**
9727
             * @psalm-suppress ImpureFunctionCall - is is only a warning
9728
             */
9729 2
            \trigger_error('UTF8::strlen() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9730
        }
9731
9732
        //
9733
        // fallback via iconv
9734
        //
9735
9736 8
        if (self::$SUPPORT['iconv'] === true) {
9737
            $return_tmp = \iconv_strlen($str, $encoding);
9738
            if ($return_tmp !== false) {
9739
                return $return_tmp;
9740
            }
9741
        }
9742
9743
        //
9744
        // fallback via intl
9745
        //
9746
9747
        if (
9748 8
            $encoding === 'UTF-8' // INFO: "grapheme_strlen()" can't handle other encodings
9749
            &&
9750 8
            self::$SUPPORT['intl'] === true
9751
        ) {
9752
            $return_tmp = \grapheme_strlen($str);
9753
            if ($return_tmp !== null) {
9754
                return $return_tmp;
9755
            }
9756
        }
9757
9758
        //
9759
        // fallback for ascii only
9760
        //
9761
9762 8
        if (ASCII::is_ascii($str)) {
9763 4
            return \strlen($str);
9764
        }
9765
9766
        //
9767
        // fallback via vanilla php
9768
        //
9769
9770 8
        \preg_match_all('/./us', $str, $parts);
9771
9772 8
        $return_tmp = \count($parts[0]);
9773 8
        if ($return_tmp === 0) {
9774
            return false;
9775
        }
9776
9777 8
        return $return_tmp;
9778
    }
9779
9780
    /**
9781
     * Get string length in byte.
9782
     *
9783
     * @param string $str
9784
     *
9785
     * @psalm-pure
9786
     *
9787
     * @return int
9788
     */
9789 1
    public static function strlen_in_byte(string $str): int
9790
    {
9791 1
        if ($str === '') {
9792
            return 0;
9793
        }
9794
9795 1
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9796
            // "mb_" is available if overload is used, so use it ...
9797
            return \mb_strlen($str, 'CP850'); // 8-BIT
9798
        }
9799
9800 1
        return \strlen($str);
9801
    }
9802
9803
    /**
9804
     * Case-insensitive string comparisons using a "natural order" algorithm.
9805
     *
9806
     * INFO: natural order version of UTF8::strcasecmp()
9807
     *
9808
     * @param string $str1     <p>The first string.</p>
9809
     * @param string $str2     <p>The second string.</p>
9810
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9811
     *
9812
     * @psalm-pure
9813
     *
9814
     * @return int
9815
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
9816
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
9817
     *             <strong>0</strong> if they are equal
9818
     */
9819 2
    public static function strnatcasecmp(string $str1, string $str2, string $encoding = 'UTF-8'): int
9820
    {
9821 2
        return self::strnatcmp(
9822 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
9823 2
            self::strtocasefold($str2, true, false, $encoding, null, false)
9824
        );
9825
    }
9826
9827
    /**
9828
     * String comparisons using a "natural order" algorithm
9829
     *
9830
     * INFO: natural order version of UTF8::strcmp()
9831
     *
9832
     * @see http://php.net/manual/en/function.strnatcmp.php
9833
     *
9834
     * @param string $str1 <p>The first string.</p>
9835
     * @param string $str2 <p>The second string.</p>
9836
     *
9837
     * @psalm-pure
9838
     *
9839
     * @return int
9840
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
9841
     *             <strong>&gt; 0</strong> if str1 is greater than str2;<br>
9842
     *             <strong>0</strong> if they are equal
9843
     */
9844 4
    public static function strnatcmp(string $str1, string $str2): int
9845
    {
9846 4
        if ($str1 === $str2) {
9847 4
            return 0;
9848
        }
9849
9850 4
        return \strnatcmp(
9851 4
            (string) self::strtonatfold($str1),
9852 4
            (string) self::strtonatfold($str2)
9853
        );
9854
    }
9855
9856
    /**
9857
     * Case-insensitive string comparison of the first n characters.
9858
     *
9859
     * @see http://php.net/manual/en/function.strncasecmp.php
9860
     *
9861
     * @param string $str1     <p>The first string.</p>
9862
     * @param string $str2     <p>The second string.</p>
9863
     * @param int    $len      <p>The length of strings to be used in the comparison.</p>
9864
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9865
     *
9866
     * @psalm-pure
9867
     *
9868
     * @return int
9869
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
9870
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
9871
     *             <strong>0</strong> if they are equal
9872
     */
9873 2
    public static function strncasecmp(
9874
        string $str1,
9875
        string $str2,
9876
        int $len,
9877
        string $encoding = 'UTF-8'
9878
    ): int {
9879 2
        return self::strncmp(
9880 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
9881 2
            self::strtocasefold($str2, true, false, $encoding, null, false),
9882 2
            $len
9883
        );
9884
    }
9885
9886
    /**
9887
     * String comparison of the first n characters.
9888
     *
9889
     * @see http://php.net/manual/en/function.strncmp.php
9890
     *
9891
     * @param string $str1     <p>The first string.</p>
9892
     * @param string $str2     <p>The second string.</p>
9893
     * @param int    $len      <p>Number of characters to use in the comparison.</p>
9894
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9895
     *
9896
     * @psalm-pure
9897
     *
9898
     * @return int
9899
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
9900
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
9901
     *             <strong>0</strong> if they are equal
9902
     */
9903 4
    public static function strncmp(
9904
        string $str1,
9905
        string $str2,
9906
        int $len,
9907
        string $encoding = 'UTF-8'
9908
    ): int {
9909 4
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9910
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9911
        }
9912
9913 4
        if ($encoding === 'UTF-8') {
9914 4
            $str1 = (string) \mb_substr($str1, 0, $len);
9915 4
            $str2 = (string) \mb_substr($str2, 0, $len);
9916
        } else {
9917
            $str1 = (string) self::substr($str1, 0, $len, $encoding);
9918
            $str2 = (string) self::substr($str2, 0, $len, $encoding);
9919
        }
9920
9921 4
        return self::strcmp($str1, $str2);
9922
    }
9923
9924
    /**
9925
     * Search a string for any of a set of characters.
9926
     *
9927
     * @see http://php.net/manual/en/function.strpbrk.php
9928
     *
9929
     * @param string $haystack  <p>The string where char_list is looked for.</p>
9930
     * @param string $char_list <p>This parameter is case-sensitive.</p>
9931
     *
9932
     * @psalm-pure
9933
     *
9934
     * @return false|string
9935
     *                      <p>The string starting from the character found, or false if it is not found.</p>
9936
     */
9937 2
    public static function strpbrk(string $haystack, string $char_list)
9938
    {
9939 2
        if ($haystack === '' || $char_list === '') {
9940 2
            return false;
9941
        }
9942
9943 2
        if (\preg_match('/' . self::rxClass($char_list) . '/us', $haystack, $m)) {
9944 2
            return \substr($haystack, (int) \strpos($haystack, $m[0]));
9945
        }
9946
9947 2
        return false;
9948
    }
9949
9950
    /**
9951
     * Find the position of the first occurrence of a substring in a string.
9952
     *
9953
     * @see http://php.net/manual/en/function.mb-strpos.php
9954
     *
9955
     * @param string     $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
9956
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
9957
     * @param int        $offset     [optional] <p>The search offset. If it is not specified, 0 is used.</p>
9958
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9959
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9960
     *
9961
     * @psalm-pure
9962
     *
9963
     * @return false|int
9964
     *                   The <strong>(int)</strong> numeric position of the first occurrence of needle in the haystack
9965
     *                   string.<br> If needle is not found it returns false.
9966
     */
9967 53
    public static function strpos(
9968
        string $haystack,
9969
        $needle,
9970
        int $offset = 0,
9971
        string $encoding = 'UTF-8',
9972
        bool $clean_utf8 = false
9973
    ) {
9974 53
        if ($haystack === '') {
9975 4
            return false;
9976
        }
9977
9978
        // iconv and mbstring do not support integer $needle
9979 52
        if ((int) $needle === $needle) {
9980
            $needle = (string) self::chr($needle);
9981
        }
9982 52
        $needle = (string) $needle;
9983
9984 52
        if ($needle === '') {
9985 2
            return false;
9986
        }
9987
9988 52
        if ($clean_utf8) {
9989
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9990
            // if invalid characters are found in $haystack before $needle
9991 3
            $needle = self::clean($needle);
9992 3
            $haystack = self::clean($haystack);
9993
        }
9994
9995 52
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9996 10
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9997
        }
9998
9999
        //
10000
        // fallback via mbstring
10001
        //
10002
10003 52
        if (self::$SUPPORT['mbstring'] === true) {
10004 50
            if ($encoding === 'UTF-8') {
10005 50
                return \mb_strpos($haystack, $needle, $offset);
10006
            }
10007
10008 2
            return \mb_strpos($haystack, $needle, $offset, $encoding);
10009
        }
10010
10011
        //
10012
        // fallback for binary || ascii only
10013
        //
10014
        if (
10015 4
            $encoding === 'CP850'
10016
            ||
10017 4
            $encoding === 'ASCII'
10018
        ) {
10019 2
            return \strpos($haystack, $needle, $offset);
10020
        }
10021
10022
        if (
10023 4
            $encoding !== 'UTF-8'
10024
            &&
10025 4
            self::$SUPPORT['iconv'] === false
10026
            &&
10027 4
            self::$SUPPORT['mbstring'] === false
10028
        ) {
10029
            /**
10030
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10031
             */
10032 2
            \trigger_error('UTF8::strpos() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10033
        }
10034
10035
        //
10036
        // fallback via intl
10037
        //
10038
10039
        if (
10040 4
            $encoding === 'UTF-8' // INFO: "grapheme_strpos()" can't handle other encodings
10041
            &&
10042 4
            $offset >= 0 // grapheme_strpos() can't handle negative offset
10043
            &&
10044 4
            self::$SUPPORT['intl'] === true
10045
        ) {
10046
            $return_tmp = \grapheme_strpos($haystack, $needle, $offset);
10047
            if ($return_tmp !== false) {
10048
                return $return_tmp;
10049
            }
10050
        }
10051
10052
        //
10053
        // fallback via iconv
10054
        //
10055
10056
        if (
10057 4
            $offset >= 0 // iconv_strpos() can't handle negative offset
10058
            &&
10059 4
            self::$SUPPORT['iconv'] === true
10060
        ) {
10061
            // ignore invalid negative offset to keep compatibility
10062
            // with php < 5.5.35, < 5.6.21, < 7.0.6
10063
            $return_tmp = \iconv_strpos($haystack, $needle, $offset > 0 ? $offset : 0, $encoding);
10064
            if ($return_tmp !== false) {
10065
                return $return_tmp;
10066
            }
10067
        }
10068
10069
        //
10070
        // fallback for ascii only
10071
        //
10072
10073 4
        if (ASCII::is_ascii($haystack . $needle)) {
10074 2
            return \strpos($haystack, $needle, $offset);
10075
        }
10076
10077
        //
10078
        // fallback via vanilla php
10079
        //
10080
10081 4
        $haystack_tmp = self::substr($haystack, $offset, null, $encoding);
10082 4
        if ($haystack_tmp === false) {
10083
            $haystack_tmp = '';
10084
        }
10085 4
        $haystack = (string) $haystack_tmp;
10086
10087 4
        if ($offset < 0) {
10088
            $offset = 0;
10089
        }
10090
10091 4
        $pos = \strpos($haystack, $needle);
10092 4
        if ($pos === false) {
10093 2
            return false;
10094
        }
10095
10096 4
        if ($pos) {
10097 4
            return $offset + (int) self::strlen(\substr($haystack, 0, $pos), $encoding);
10098
        }
10099
10100 2
        return $offset + 0;
10101
    }
10102
10103
    /**
10104
     * Find the position of the first occurrence of a substring in a string.
10105
     *
10106
     * @param string $haystack <p>
10107
     *                         The string being checked.
10108
     *                         </p>
10109
     * @param string $needle   <p>
10110
     *                         The position counted from the beginning of haystack.
10111
     *                         </p>
10112
     * @param int    $offset   [optional] <p>
10113
     *                         The search offset. If it is not specified, 0 is used.
10114
     *                         </p>
10115
     *
10116
     * @psalm-pure
10117
     *
10118
     * @return false|int
10119
     *                   <p>The numeric position of the first occurrence of needle in the
10120
     *                   haystack string. If needle is not found, it returns false.</p>
10121
     */
10122 2
    public static function strpos_in_byte(string $haystack, string $needle, int $offset = 0)
10123
    {
10124 2
        if ($haystack === '' || $needle === '') {
10125
            return false;
10126
        }
10127
10128 2
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10129
            // "mb_" is available if overload is used, so use it ...
10130
            return \mb_strpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
10131
        }
10132
10133 2
        return \strpos($haystack, $needle, $offset);
10134
    }
10135
10136
    /**
10137
     * Find the position of the first occurrence of a substring in a string, case-insensitive.
10138
     *
10139
     * @param string $haystack <p>
10140
     *                         The string being checked.
10141
     *                         </p>
10142
     * @param string $needle   <p>
10143
     *                         The position counted from the beginning of haystack.
10144
     *                         </p>
10145
     * @param int    $offset   [optional] <p>
10146
     *                         The search offset. If it is not specified, 0 is used.
10147
     *                         </p>
10148
     *
10149
     * @psalm-pure
10150
     *
10151
     * @return false|int
10152
     *                   <p>The numeric position of the first occurrence of needle in the
10153
     *                   haystack string. If needle is not found, it returns false.</p>
10154
     */
10155 2
    public static function stripos_in_byte(string $haystack, string $needle, int $offset = 0)
10156
    {
10157 2
        if ($haystack === '' || $needle === '') {
10158
            return false;
10159
        }
10160
10161 2
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10162
            // "mb_" is available if overload is used, so use it ...
10163
            return \mb_stripos($haystack, $needle, $offset, 'CP850'); // 8-BIT
10164
        }
10165
10166 2
        return \stripos($haystack, $needle, $offset);
10167
    }
10168
10169
    /**
10170
     * Find the last occurrence of a character in a string within another.
10171
     *
10172
     * @see http://php.net/manual/en/function.mb-strrchr.php
10173
     *
10174
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
10175
     * @param string $needle        <p>The string to find in haystack</p>
10176
     * @param bool   $before_needle [optional] <p>
10177
     *                              Determines which portion of haystack
10178
     *                              this function returns.
10179
     *                              If set to true, it returns all of haystack
10180
     *                              from the beginning to the last occurrence of needle.
10181
     *                              If set to false, it returns all of haystack
10182
     *                              from the last occurrence of needle to the end,
10183
     *                              </p>
10184
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
10185
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
10186
     *
10187
     * @psalm-pure
10188
     *
10189
     * @return false|string
10190
     *                      <p>The portion of haystack or false if needle is not found.</p>
10191
     */
10192 2
    public static function strrchr(
10193
        string $haystack,
10194
        string $needle,
10195
        bool $before_needle = false,
10196
        string $encoding = 'UTF-8',
10197
        bool $clean_utf8 = false
10198
    ) {
10199 2
        if ($haystack === '' || $needle === '') {
10200 2
            return false;
10201
        }
10202
10203 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10204 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10205
        }
10206
10207 2
        if ($clean_utf8) {
10208
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10209
            // if invalid characters are found in $haystack before $needle
10210 2
            $needle = self::clean($needle);
10211 2
            $haystack = self::clean($haystack);
10212
        }
10213
10214
        //
10215
        // fallback via mbstring
10216
        //
10217
10218 2
        if (self::$SUPPORT['mbstring'] === true) {
10219 2
            if ($encoding === 'UTF-8') {
10220 2
                return \mb_strrchr($haystack, $needle, $before_needle);
10221
            }
10222
10223 2
            return \mb_strrchr($haystack, $needle, $before_needle, $encoding);
10224
        }
10225
10226
        //
10227
        // fallback for binary || ascii only
10228
        //
10229
10230
        if (
10231
            !$before_needle
10232
            &&
10233
            (
10234
                $encoding === 'CP850'
10235
                ||
10236
                $encoding === 'ASCII'
10237
            )
10238
        ) {
10239
            return \strrchr($haystack, $needle);
10240
        }
10241
10242
        if (
10243
            $encoding !== 'UTF-8'
10244
            &&
10245
            self::$SUPPORT['mbstring'] === false
10246
        ) {
10247
            /**
10248
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10249
             */
10250
            \trigger_error('UTF8::strrchr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10251
        }
10252
10253
        //
10254
        // fallback via iconv
10255
        //
10256
10257
        if (self::$SUPPORT['iconv'] === true) {
10258
            $needle_tmp = self::substr($needle, 0, 1, $encoding);
10259
            if ($needle_tmp === false) {
10260
                return false;
10261
            }
10262
            $needle = (string) $needle_tmp;
10263
10264
            $pos = \iconv_strrpos($haystack, $needle, $encoding);
10265
            if ($pos === false) {
10266
                return false;
10267
            }
10268
10269
            if ($before_needle) {
10270
                return self::substr($haystack, 0, $pos, $encoding);
10271
            }
10272
10273
            return self::substr($haystack, $pos, null, $encoding);
10274
        }
10275
10276
        //
10277
        // fallback via vanilla php
10278
        //
10279
10280
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
10281
        if ($needle_tmp === false) {
10282
            return false;
10283
        }
10284
        $needle = (string) $needle_tmp;
10285
10286
        $pos = self::strrpos($haystack, $needle, 0, $encoding);
10287
        if ($pos === false) {
10288
            return false;
10289
        }
10290
10291
        if ($before_needle) {
10292
            return self::substr($haystack, 0, $pos, $encoding);
10293
        }
10294
10295
        return self::substr($haystack, $pos, null, $encoding);
10296
    }
10297
10298
    /**
10299
     * Reverses characters order in the string.
10300
     *
10301
     * @param string $str      <p>The input string.</p>
10302
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
10303
     *
10304
     * @psalm-pure
10305
     *
10306
     * @return string
10307
     *                <p>The string with characters in the reverse sequence.</p>
10308
     */
10309 10
    public static function strrev(string $str, string $encoding = 'UTF-8'): string
10310
    {
10311 10
        if ($str === '') {
10312 4
            return '';
10313
        }
10314
10315
        // init
10316 8
        $reversed = '';
10317
10318 8
        $str = self::emoji_encode($str, true);
10319
10320 8
        if ($encoding === 'UTF-8') {
10321 8
            if (self::$SUPPORT['intl'] === true) {
10322
                // try "grapheme" first: https://stackoverflow.com/questions/17496493/strrev-dosent-support-utf-8
10323 8
                $i = (int) \grapheme_strlen($str);
10324 8
                while ($i--) {
10325 8
                    $reversed_tmp = \grapheme_substr($str, $i, 1);
10326 8
                    if ($reversed_tmp !== false) {
10327 8
                        $reversed .= $reversed_tmp;
10328
                    }
10329
                }
10330
            } else {
10331
                $i = (int) \mb_strlen($str);
10332 8
                while ($i--) {
10333
                    $reversed_tmp = \mb_substr($str, $i, 1);
10334
                    if ($reversed_tmp !== false) {
10335
                        $reversed .= $reversed_tmp;
10336
                    }
10337
                }
10338
            }
10339
        } else {
10340
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10341
10342
            $i = (int) self::strlen($str, $encoding);
10343
            while ($i--) {
10344
                $reversed_tmp = self::substr($str, $i, 1, $encoding);
10345
                if ($reversed_tmp !== false) {
10346
                    $reversed .= $reversed_tmp;
10347
                }
10348
            }
10349
        }
10350
10351 8
        return self::emoji_decode($reversed, true);
10352
    }
10353
10354
    /**
10355
     * Find the last occurrence of a character in a string within another, case-insensitive.
10356
     *
10357
     * @see http://php.net/manual/en/function.mb-strrichr.php
10358
     *
10359
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
10360
     * @param string $needle        <p>The string to find in haystack.</p>
10361
     * @param bool   $before_needle [optional] <p>
10362
     *                              Determines which portion of haystack
10363
     *                              this function returns.
10364
     *                              If set to true, it returns all of haystack
10365
     *                              from the beginning to the last occurrence of needle.
10366
     *                              If set to false, it returns all of haystack
10367
     *                              from the last occurrence of needle to the end,
10368
     *                              </p>
10369
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
10370
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
10371
     *
10372
     * @psalm-pure
10373
     *
10374
     * @return false|string
10375
     *                      <p>The portion of haystack or<br>false if needle is not found.</p>
10376
     */
10377 3
    public static function strrichr(
10378
        string $haystack,
10379
        string $needle,
10380
        bool $before_needle = false,
10381
        string $encoding = 'UTF-8',
10382
        bool $clean_utf8 = false
10383
    ) {
10384 3
        if ($haystack === '' || $needle === '') {
10385 2
            return false;
10386
        }
10387
10388 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10389 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10390
        }
10391
10392 3
        if ($clean_utf8) {
10393
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10394
            // if invalid characters are found in $haystack before $needle
10395 2
            $needle = self::clean($needle);
10396 2
            $haystack = self::clean($haystack);
10397
        }
10398
10399
        //
10400
        // fallback via mbstring
10401
        //
10402
10403 3
        if (self::$SUPPORT['mbstring'] === true) {
10404 3
            if ($encoding === 'UTF-8') {
10405 3
                return \mb_strrichr($haystack, $needle, $before_needle);
10406
            }
10407
10408 2
            return \mb_strrichr($haystack, $needle, $before_needle, $encoding);
10409
        }
10410
10411
        //
10412
        // fallback via vanilla php
10413
        //
10414
10415
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
10416
        if ($needle_tmp === false) {
10417
            return false;
10418
        }
10419
        $needle = (string) $needle_tmp;
10420
10421
        $pos = self::strripos($haystack, $needle, 0, $encoding);
10422
        if ($pos === false) {
10423
            return false;
10424
        }
10425
10426
        if ($before_needle) {
10427
            return self::substr($haystack, 0, $pos, $encoding);
10428
        }
10429
10430
        return self::substr($haystack, $pos, null, $encoding);
10431
    }
10432
10433
    /**
10434
     * Find the position of the last occurrence of a substring in a string, case-insensitive.
10435
     *
10436
     * @param string     $haystack   <p>The string to look in.</p>
10437
     * @param int|string $needle     <p>The string to look for.</p>
10438
     * @param int        $offset     [optional] <p>Number of characters to ignore in the beginning or end.</p>
10439
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10440
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10441
     *
10442
     * @psalm-pure
10443
     *
10444
     * @return false|int
10445
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
10446
     *                   string.<br>If needle is not found, it returns false.</p>
10447
     */
10448 14
    public static function strripos(
10449
        string $haystack,
10450
        $needle,
10451
        int $offset = 0,
10452
        string $encoding = 'UTF-8',
10453
        bool $clean_utf8 = false
10454
    ) {
10455 14
        if ($haystack === '') {
10456
            return false;
10457
        }
10458
10459
        // iconv and mbstring do not support integer $needle
10460 14
        if ((int) $needle === $needle && $needle >= 0) {
10461
            $needle = (string) self::chr($needle);
10462
        }
10463 14
        $needle = (string) $needle;
10464
10465 14
        if ($needle === '') {
10466
            return false;
10467
        }
10468
10469 14
        if ($clean_utf8) {
10470
            // mb_strripos() && iconv_strripos() is not tolerant to invalid characters
10471 3
            $needle = self::clean($needle);
10472 3
            $haystack = self::clean($haystack);
10473
        }
10474
10475 14
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10476 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10477
        }
10478
10479
        //
10480
        // fallback via mbstrig
10481
        //
10482
10483 14
        if (self::$SUPPORT['mbstring'] === true) {
10484 14
            if ($encoding === 'UTF-8') {
10485 14
                return \mb_strripos($haystack, $needle, $offset);
10486
            }
10487
10488
            return \mb_strripos($haystack, $needle, $offset, $encoding);
10489
        }
10490
10491
        //
10492
        // fallback for binary || ascii only
10493
        //
10494
10495
        if (
10496
            $encoding === 'CP850'
10497
            ||
10498
            $encoding === 'ASCII'
10499
        ) {
10500
            return \strripos($haystack, $needle, $offset);
10501
        }
10502
10503
        if (
10504
            $encoding !== 'UTF-8'
10505
            &&
10506
            self::$SUPPORT['mbstring'] === false
10507
        ) {
10508
            /**
10509
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10510
             */
10511
            \trigger_error('UTF8::strripos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10512
        }
10513
10514
        //
10515
        // fallback via intl
10516
        //
10517
10518
        if (
10519
            $encoding === 'UTF-8' // INFO: "grapheme_strripos()" can't handle other encodings
10520
            &&
10521
            $offset >= 0 // grapheme_strripos() can't handle negative offset
10522
            &&
10523
            self::$SUPPORT['intl'] === true
10524
        ) {
10525
            $return_tmp = \grapheme_strripos($haystack, $needle, $offset);
10526
            if ($return_tmp !== false) {
10527
                return $return_tmp;
10528
            }
10529
        }
10530
10531
        //
10532
        // fallback for ascii only
10533
        //
10534
10535
        if (ASCII::is_ascii($haystack . $needle)) {
10536
            return \strripos($haystack, $needle, $offset);
10537
        }
10538
10539
        //
10540
        // fallback via vanilla php
10541
        //
10542
10543
        $haystack = self::strtocasefold($haystack, true, false, $encoding);
10544
        $needle = self::strtocasefold($needle, true, false, $encoding);
10545
10546
        return self::strrpos($haystack, $needle, $offset, $encoding, $clean_utf8);
10547
    }
10548
10549
    /**
10550
     * Finds position of last occurrence of a string within another, case-insensitive.
10551
     *
10552
     * @param string $haystack <p>
10553
     *                         The string from which to get the position of the last occurrence
10554
     *                         of needle.
10555
     *                         </p>
10556
     * @param string $needle   <p>
10557
     *                         The string to find in haystack.
10558
     *                         </p>
10559
     * @param int    $offset   [optional] <p>
10560
     *                         The position in haystack
10561
     *                         to start searching.
10562
     *                         </p>
10563
     *
10564
     * @psalm-pure
10565
     *
10566
     * @return false|int
10567
     *                   <p>eturn the numeric position of the last occurrence of needle in the
10568
     *                   haystack string, or false if needle is not found.</p>
10569
     */
10570 2
    public static function strripos_in_byte(string $haystack, string $needle, int $offset = 0)
10571
    {
10572 2
        if ($haystack === '' || $needle === '') {
10573
            return false;
10574
        }
10575
10576 2
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10577
            // "mb_" is available if overload is used, so use it ...
10578
            return \mb_strripos($haystack, $needle, $offset, 'CP850'); // 8-BIT
10579
        }
10580
10581 2
        return \strripos($haystack, $needle, $offset);
10582
    }
10583
10584
    /**
10585
     * Find the position of the last occurrence of a substring in a string.
10586
     *
10587
     * @see http://php.net/manual/en/function.mb-strrpos.php
10588
     *
10589
     * @param string     $haystack   <p>The string being checked, for the last occurrence of needle</p>
10590
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
10591
     * @param int        $offset     [optional] <p>May be specified to begin searching an arbitrary number of characters
10592
     *                               into the string. Negative values will stop searching at an arbitrary point prior to
10593
     *                               the end of the string.
10594
     *                               </p>
10595
     * @param string     $encoding   [optional] <p>Set the charset.</p>
10596
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10597
     *
10598
     * @psalm-pure
10599
     *
10600
     * @return false|int
10601
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
10602
     *                   string.<br>If needle is not found, it returns false.</p>
10603
     */
10604 35
    public static function strrpos(
10605
        string $haystack,
10606
        $needle,
10607
        int $offset = 0,
10608
        string $encoding = 'UTF-8',
10609
        bool $clean_utf8 = false
10610
    ) {
10611 35
        if ($haystack === '') {
10612 3
            return false;
10613
        }
10614
10615
        // iconv and mbstring do not support integer $needle
10616 34
        if ((int) $needle === $needle && $needle >= 0) {
10617 1
            $needle = (string) self::chr($needle);
10618
        }
10619 34
        $needle = (string) $needle;
10620
10621 34
        if ($needle === '') {
10622 2
            return false;
10623
        }
10624
10625 34
        if ($clean_utf8) {
10626
            // mb_strrpos && iconv_strrpos is not tolerant to invalid characters
10627 4
            $needle = self::clean($needle);
10628 4
            $haystack = self::clean($haystack);
10629
        }
10630
10631 34
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10632 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10633
        }
10634
10635
        //
10636
        // fallback via mbstring
10637
        //
10638
10639 34
        if (self::$SUPPORT['mbstring'] === true) {
10640 34
            if ($encoding === 'UTF-8') {
10641 34
                return \mb_strrpos($haystack, $needle, $offset);
10642
            }
10643
10644 2
            return \mb_strrpos($haystack, $needle, $offset, $encoding);
10645
        }
10646
10647
        //
10648
        // fallback for binary || ascii only
10649
        //
10650
10651
        if (
10652
            $encoding === 'CP850'
10653
            ||
10654
            $encoding === 'ASCII'
10655
        ) {
10656
            return \strrpos($haystack, $needle, $offset);
10657
        }
10658
10659
        if (
10660
            $encoding !== 'UTF-8'
10661
            &&
10662
            self::$SUPPORT['mbstring'] === false
10663
        ) {
10664
            /**
10665
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10666
             */
10667
            \trigger_error('UTF8::strrpos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10668
        }
10669
10670
        //
10671
        // fallback via intl
10672
        //
10673
10674
        if (
10675
            $offset >= 0 // grapheme_strrpos() can't handle negative offset
10676
            &&
10677
            $encoding === 'UTF-8' // INFO: "grapheme_strrpos()" can't handle other encodings
10678
            &&
10679
            self::$SUPPORT['intl'] === true
10680
        ) {
10681
            $return_tmp = \grapheme_strrpos($haystack, $needle, $offset);
10682
            if ($return_tmp !== false) {
10683
                return $return_tmp;
10684
            }
10685
        }
10686
10687
        //
10688
        // fallback for ascii only
10689
        //
10690
10691
        if (ASCII::is_ascii($haystack . $needle)) {
10692
            return \strrpos($haystack, $needle, $offset);
10693
        }
10694
10695
        //
10696
        // fallback via vanilla php
10697
        //
10698
10699
        $haystack_tmp = null;
10700
        if ($offset > 0) {
10701
            $haystack_tmp = self::substr($haystack, $offset);
10702
        } elseif ($offset < 0) {
10703
            $haystack_tmp = self::substr($haystack, 0, $offset);
10704
            $offset = 0;
10705
        }
10706
10707
        if ($haystack_tmp !== null) {
10708
            if ($haystack_tmp === false) {
10709
                $haystack_tmp = '';
10710
            }
10711
            $haystack = (string) $haystack_tmp;
10712
        }
10713
10714
        $pos = \strrpos($haystack, $needle);
10715
        if ($pos === false) {
10716
            return false;
10717
        }
10718
10719
        /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
10720
        $str_tmp = \substr($haystack, 0, $pos);
10721
        if ($str_tmp === false) {
10722
            return false;
10723
        }
10724
10725
        return $offset + (int) self::strlen($str_tmp);
10726
    }
10727
10728
    /**
10729
     * Find the position of the last occurrence of a substring in a string.
10730
     *
10731
     * @param string $haystack <p>
10732
     *                         The string being checked, for the last occurrence
10733
     *                         of needle.
10734
     *                         </p>
10735
     * @param string $needle   <p>
10736
     *                         The string to find in haystack.
10737
     *                         </p>
10738
     * @param int    $offset   [optional] <p>May be specified to begin searching an arbitrary number of characters into
10739
     *                         the string. Negative values will stop searching at an arbitrary point
10740
     *                         prior to the end of the string.
10741
     *                         </p>
10742
     *
10743
     * @psalm-pure
10744
     *
10745
     * @return false|int
10746
     *                   <p>The numeric position of the last occurrence of needle in the
10747
     *                   haystack string. If needle is not found, it returns false.</p>
10748
     */
10749 2
    public static function strrpos_in_byte(string $haystack, string $needle, int $offset = 0)
10750
    {
10751 2
        if ($haystack === '' || $needle === '') {
10752
            return false;
10753
        }
10754
10755 2
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10756
            // "mb_" is available if overload is used, so use it ...
10757
            return \mb_strrpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
10758
        }
10759
10760 2
        return \strrpos($haystack, $needle, $offset);
10761
    }
10762
10763
    /**
10764
     * Finds the length of the initial segment of a string consisting entirely of characters contained within a given
10765
     * mask.
10766
     *
10767
     * @param string $str      <p>The input string.</p>
10768
     * @param string $mask     <p>The mask of chars</p>
10769
     * @param int    $offset   [optional]
10770
     * @param int    $length   [optional]
10771
     * @param string $encoding [optional] <p>Set the charset.</p>
10772
     *
10773
     * @psalm-pure
10774
     *
10775
     * @return false|int
10776
     */
10777 10
    public static function strspn(
10778
        string $str,
10779
        string $mask,
10780
        int $offset = 0,
10781
        int $length = null,
10782
        string $encoding = 'UTF-8'
10783
    ) {
10784 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10785
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10786
        }
10787
10788 10
        if ($offset || $length !== null) {
10789 2
            if ($encoding === 'UTF-8') {
10790 2
                if ($length === null) {
10791
                    $str = (string) \mb_substr($str, $offset);
10792
                } else {
10793 2
                    $str = (string) \mb_substr($str, $offset, $length);
10794
                }
10795
            } else {
10796
                $str = (string) self::substr($str, $offset, $length, $encoding);
10797
            }
10798
        }
10799
10800 10
        if ($str === '' || $mask === '') {
10801 2
            return 0;
10802
        }
10803
10804 8
        $matches = [];
10805
10806 8
        return \preg_match('/^' . self::rxClass($mask) . '+/u', $str, $matches) ? (int) self::strlen($matches[0], $encoding) : 0;
10807
    }
10808
10809
    /**
10810
     * Returns part of haystack string from the first occurrence of needle to the end of haystack.
10811
     *
10812
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
10813
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
10814
     * @param bool   $before_needle [optional] <p>
10815
     *                              If <b>TRUE</b>, strstr() returns the part of the
10816
     *                              haystack before the first occurrence of the needle (excluding the needle).
10817
     *                              </p>
10818
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
10819
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
10820
     *
10821
     * @psalm-pure
10822
     *
10823
     * @return false|string
10824
     *                      A sub-string,<br>or <strong>false</strong> if needle is not found
10825
     */
10826 3
    public static function strstr(
10827
        string $haystack,
10828
        string $needle,
10829
        bool $before_needle = false,
10830
        string $encoding = 'UTF-8',
10831
        bool $clean_utf8 = false
10832
    ) {
10833 3
        if ($haystack === '' || $needle === '') {
10834 2
            return false;
10835
        }
10836
10837 3
        if ($clean_utf8) {
10838
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10839
            // if invalid characters are found in $haystack before $needle
10840
            $needle = self::clean($needle);
10841
            $haystack = self::clean($haystack);
10842
        }
10843
10844 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10845 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10846
        }
10847
10848
        //
10849
        // fallback via mbstring
10850
        //
10851
10852 3
        if (self::$SUPPORT['mbstring'] === true) {
10853 3
            if ($encoding === 'UTF-8') {
10854 3
                return \mb_strstr($haystack, $needle, $before_needle);
10855
            }
10856
10857 2
            return \mb_strstr($haystack, $needle, $before_needle, $encoding);
10858
        }
10859
10860
        //
10861
        // fallback for binary || ascii only
10862
        //
10863
10864
        if (
10865
            $encoding === 'CP850'
10866
            ||
10867
            $encoding === 'ASCII'
10868
        ) {
10869
            return \strstr($haystack, $needle, $before_needle);
10870
        }
10871
10872
        if (
10873
            $encoding !== 'UTF-8'
10874
            &&
10875
            self::$SUPPORT['mbstring'] === false
10876
        ) {
10877
            /**
10878
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10879
             */
10880
            \trigger_error('UTF8::strstr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10881
        }
10882
10883
        //
10884
        // fallback via intl
10885
        //
10886
10887
        if (
10888
            $encoding === 'UTF-8' // INFO: "grapheme_strstr()" can't handle other encodings
10889
            &&
10890
            self::$SUPPORT['intl'] === true
10891
        ) {
10892
            $return_tmp = \grapheme_strstr($haystack, $needle, $before_needle);
10893
            if ($return_tmp !== false) {
10894
                return $return_tmp;
10895
            }
10896
        }
10897
10898
        //
10899
        // fallback for ascii only
10900
        //
10901
10902
        if (ASCII::is_ascii($haystack . $needle)) {
10903
            return \strstr($haystack, $needle, $before_needle);
10904
        }
10905
10906
        //
10907
        // fallback via vanilla php
10908
        //
10909
10910
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/us', $haystack, $match);
10911
10912
        if (!isset($match[1])) {
10913
            return false;
10914
        }
10915
10916
        if ($before_needle) {
10917
            return $match[1];
10918
        }
10919
10920
        return self::substr($haystack, (int) self::strlen($match[1]));
10921
    }
10922
10923
    /**
10924
     * Finds first occurrence of a string within another.
10925
     *
10926
     * @param string $haystack      <p>
10927
     *                              The string from which to get the first occurrence
10928
     *                              of needle.
10929
     *                              </p>
10930
     * @param string $needle        <p>
10931
     *                              The string to find in haystack.
10932
     *                              </p>
10933
     * @param bool   $before_needle [optional] <p>
10934
     *                              Determines which portion of haystack
10935
     *                              this function returns.
10936
     *                              If set to true, it returns all of haystack
10937
     *                              from the beginning to the first occurrence of needle.
10938
     *                              If set to false, it returns all of haystack
10939
     *                              from the first occurrence of needle to the end,
10940
     *                              </p>
10941
     *
10942
     * @psalm-pure
10943
     *
10944
     * @return false|string
10945
     *                      <p>The portion of haystack,
10946
     *                      or false if needle is not found.</p>
10947
     */
10948 2
    public static function strstr_in_byte(
10949
        string $haystack,
10950
        string $needle,
10951
        bool $before_needle = false
10952
    ) {
10953 2
        if ($haystack === '' || $needle === '') {
10954
            return false;
10955
        }
10956
10957 2
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10958
            // "mb_" is available if overload is used, so use it ...
10959
            return \mb_strstr($haystack, $needle, $before_needle, 'CP850'); // 8-BIT
10960
        }
10961
10962 2
        return \strstr($haystack, $needle, $before_needle);
10963
    }
10964
10965
    /**
10966
     * Unicode transformation for case-less matching.
10967
     *
10968
     * @see http://unicode.org/reports/tr21/tr21-5.html
10969
     *
10970
     * @param string      $str        <p>The input string.</p>
10971
     * @param bool        $full       [optional] <p>
10972
     *                                <b>true</b>, replace full case folding chars (default)<br>
10973
     *                                <b>false</b>, use only limited static array [UTF8::$COMMON_CASE_FOLD]
10974
     *                                </p>
10975
     * @param bool        $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10976
     * @param string      $encoding   [optional] <p>Set the charset.</p>
10977
     * @param string|null $lang       [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10978
     * @param bool        $lower      [optional] <p>Use lowercase string, otherwise use uppercase string. PS: uppercase
10979
     *                                is for some languages better ...</p>
10980
     *
10981
     * @psalm-pure
10982
     *
10983
     * @return string
10984
     */
10985 32
    public static function strtocasefold(
10986
        string $str,
10987
        bool $full = true,
10988
        bool $clean_utf8 = false,
10989
        string $encoding = 'UTF-8',
10990
        string $lang = null,
10991
        bool $lower = true
10992
    ): string {
10993 32
        if ($str === '') {
10994 5
            return '';
10995
        }
10996
10997 31
        if ($clean_utf8) {
10998
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10999
            // if invalid characters are found in $haystack before $needle
11000 2
            $str = self::clean($str);
11001
        }
11002
11003 31
        $str = self::fixStrCaseHelper($str, $lower, $full);
11004
11005 31
        if ($lang === null && $encoding === 'UTF-8') {
11006 31
            if ($lower) {
11007 2
                return \mb_strtolower($str);
11008
            }
11009
11010 29
            return \mb_strtoupper($str);
11011
        }
11012
11013 2
        if ($lower) {
11014
            return self::strtolower($str, $encoding, false, $lang);
11015
        }
11016
11017 2
        return self::strtoupper($str, $encoding, false, $lang);
11018
    }
11019
11020
    /**
11021
     * Make a string lowercase.
11022
     *
11023
     * @see http://php.net/manual/en/function.mb-strtolower.php
11024
     *
11025
     * @param string      $str                           <p>The string being lowercased.</p>
11026
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
11027
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
11028
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
11029
     *                                                   tr</p>
11030
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
11031
     *                                                   -> ß</p>
11032
     *
11033
     * @psalm-pure
11034
     *
11035
     * @return string
11036
     *                <p>String with all alphabetic characters converted to lowercase.</p>
11037
     */
11038 73
    public static function strtolower(
11039
        $str,
11040
        string $encoding = 'UTF-8',
11041
        bool $clean_utf8 = false,
11042
        string $lang = null,
11043
        bool $try_to_keep_the_string_length = false
11044
    ): string {
11045
        // init
11046 73
        $str = (string) $str;
11047
11048 73
        if ($str === '') {
11049 1
            return '';
11050
        }
11051
11052 72
        if ($clean_utf8) {
11053
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11054
            // if invalid characters are found in $haystack before $needle
11055 2
            $str = self::clean($str);
11056
        }
11057
11058
        // hack for old php version or for the polyfill ...
11059 72
        if ($try_to_keep_the_string_length) {
11060
            $str = self::fixStrCaseHelper($str, true);
11061
        }
11062
11063 72
        if ($lang === null && $encoding === 'UTF-8') {
11064 13
            return \mb_strtolower($str);
11065
        }
11066
11067 61
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
11068
11069 61
        if ($lang !== null) {
11070 2
            if (self::$SUPPORT['intl'] === true) {
11071 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
11072
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
11073
                }
11074
11075 2
                $language_code = $lang . '-Lower';
11076 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
11077
                    /**
11078
                     * @psalm-suppress ImpureFunctionCall - is is only a warning
11079
                     */
11080
                    \trigger_error('UTF8::strtolower() cannot handle special language: ' . $lang . ' | supported: ' . \print_r(self::$INTL_TRANSLITERATOR_LIST, true), \E_USER_WARNING);
11081
11082
                    $language_code = 'Any-Lower';
11083
                }
11084
11085
                /** @noinspection PhpComposerExtensionStubsInspection */
11086
                /** @noinspection UnnecessaryCastingInspection */
11087 2
                return (string) \transliterator_transliterate($language_code, $str);
11088
            }
11089
11090
            /**
11091
             * @psalm-suppress ImpureFunctionCall - is is only a warning
11092
             */
11093
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang" parameter: ' . $lang, \E_USER_WARNING);
11094
        }
11095
11096
        // always fallback via symfony polyfill
11097 61
        return \mb_strtolower($str, $encoding);
11098
    }
11099
11100
    /**
11101
     * Make a string uppercase.
11102
     *
11103
     * @see http://php.net/manual/en/function.mb-strtoupper.php
11104
     *
11105
     * @param string      $str                           <p>The string being uppercased.</p>
11106
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
11107
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
11108
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
11109
     *                                                   tr</p>
11110
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
11111
     *                                                   -> ß</p>
11112
     *
11113
     * @psalm-pure
11114
     *
11115
     * @return string
11116
     *                <p>String with all alphabetic characters converted to uppercase.</p>
11117
     */
11118 17
    public static function strtoupper(
11119
        $str,
11120
        string $encoding = 'UTF-8',
11121
        bool $clean_utf8 = false,
11122
        string $lang = null,
11123
        bool $try_to_keep_the_string_length = false
11124
    ): string {
11125
        // init
11126 17
        $str = (string) $str;
11127
11128 17
        if ($str === '') {
11129 1
            return '';
11130
        }
11131
11132 16
        if ($clean_utf8) {
11133
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11134
            // if invalid characters are found in $haystack before $needle
11135 2
            $str = self::clean($str);
11136
        }
11137
11138
        // hack for old php version or for the polyfill ...
11139 16
        if ($try_to_keep_the_string_length) {
11140 2
            $str = self::fixStrCaseHelper($str, false);
11141
        }
11142
11143 16
        if ($lang === null && $encoding === 'UTF-8') {
11144 8
            return \mb_strtoupper($str);
11145
        }
11146
11147 10
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
11148
11149 10
        if ($lang !== null) {
11150 2
            if (self::$SUPPORT['intl'] === true) {
11151 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
11152
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
11153
                }
11154
11155 2
                $language_code = $lang . '-Upper';
11156 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
11157
                    /**
11158
                     * @psalm-suppress ImpureFunctionCall - is is only a warning
11159
                     */
11160
                    \trigger_error('UTF8::strtoupper() without intl for special language: ' . $lang, \E_USER_WARNING);
11161
11162
                    $language_code = 'Any-Upper';
11163
                }
11164
11165
                /** @noinspection PhpComposerExtensionStubsInspection */
11166
                /** @noinspection UnnecessaryCastingInspection */
11167 2
                return (string) \transliterator_transliterate($language_code, $str);
11168
            }
11169
11170
            /**
11171
             * @psalm-suppress ImpureFunctionCall - is is only a warning
11172
             */
11173
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang"-parameter: ' . $lang, \E_USER_WARNING);
11174
        }
11175
11176
        // always fallback via symfony polyfill
11177 10
        return \mb_strtoupper($str, $encoding);
11178
    }
11179
11180
    /**
11181
     * Translate characters or replace sub-strings.
11182
     *
11183
     * <p>
11184
     * <br>
11185
     * Examples:
11186
     * <br>
11187
     * <br>
11188
     * <code>
11189
     * UTF8::strtr(string $str, string $from, string $to): string
11190
     * </code>
11191
     * <br><br>
11192
     * <code>
11193
     * UTF8::strtr(string $str, array $replace_pairs): string
11194
     * </code>
11195
     * </p>
11196
     *
11197
     * @see http://php.net/manual/en/function.strtr.php
11198
     *
11199
     * @param string          $str  <p>The string being translated.</p>
11200
     * @param string|string[] $from <p>The string replacing from.</p>
11201
     * @param string|string[] $to   [optional] <p>The string being translated to to.</p>
11202
     *
11203
     * @psalm-pure
11204
     *
11205
     * @return string
11206
     *                <p>This function returns a copy of str, translating all occurrences of each character in "from"
11207
     *                to the corresponding character in "to".</p>
11208
     */
11209 2
    public static function strtr(string $str, $from, $to = ''): string
11210
    {
11211 2
        if ($str === '') {
11212
            return '';
11213
        }
11214
11215 2
        if ($from === $to) {
11216
            return $str;
11217
        }
11218
11219 2
        if ($to !== '') {
11220 2
            if (!\is_array($from)) {
11221 2
                $from = self::str_split($from);
11222
            }
11223
11224 2
            if (!\is_array($to)) {
11225 2
                $to = self::str_split($to);
11226
            }
11227
11228 2
            $count_from = \count($from);
11229 2
            $count_to = \count($to);
11230
11231 2
            if ($count_from !== $count_to) {
11232 2
                if ($count_from > $count_to) {
11233 2
                    $from = \array_slice($from, 0, $count_to);
11234 2
                } elseif ($count_from < $count_to) {
11235 2
                    $to = \array_slice($to, 0, $count_from);
11236
                }
11237
            }
11238
11239 2
            $from = \array_combine($from, $to);
11240
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
11241 2
            if ($from === false) {
11242
                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) . ')');
11243
            }
11244
        }
11245
11246 2
        if (\is_string($from)) {
11247 2
            return \str_replace($from, $to, $str);
0 ignored issues
show
Bug introduced by
It seems like $to can also be of type array<mixed,array> and array<mixed,string[]>; however, parameter $replace of str_replace() does only seem to accept string|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

11247
            return \str_replace($from, /** @scrutinizer ignore-type */ $to, $str);
Loading history...
11248
        }
11249
11250 2
        return \strtr($str, $from);
11251
    }
11252
11253
    /**
11254
     * Return the width of a string.
11255
     *
11256
     * @param string $str        <p>The input string.</p>
11257
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11258
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11259
     *
11260
     * @psalm-pure
11261
     *
11262
     * @return int
11263
     */
11264 2
    public static function strwidth(
11265
        string $str,
11266
        string $encoding = 'UTF-8',
11267
        bool $clean_utf8 = false
11268
    ): int {
11269 2
        if ($str === '') {
11270 2
            return 0;
11271
        }
11272
11273 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
11274 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11275
        }
11276
11277 2
        if ($clean_utf8) {
11278
            // iconv and mbstring are not tolerant to invalid encoding
11279
            // further, their behaviour is inconsistent with that of PHP's substr
11280 2
            $str = self::clean($str);
11281
        }
11282
11283
        //
11284
        // fallback via mbstring
11285
        //
11286
11287 2
        if (self::$SUPPORT['mbstring'] === true) {
11288 2
            if ($encoding === 'UTF-8') {
11289 2
                return \mb_strwidth($str);
11290
            }
11291
11292
            return \mb_strwidth($str, $encoding);
11293
        }
11294
11295
        //
11296
        // fallback via vanilla php
11297
        //
11298
11299
        if ($encoding !== 'UTF-8') {
11300
            $str = self::encode('UTF-8', $str, false, $encoding);
11301
        }
11302
11303
        $wide = 0;
11304
        $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);
11305
11306
        return ($wide << 1) + (int) self::strlen($str, 'UTF-8');
11307
    }
11308
11309
    /**
11310
     * Get part of a string.
11311
     *
11312
     * @see http://php.net/manual/en/function.mb-substr.php
11313
     *
11314
     * @param string $str        <p>The string being checked.</p>
11315
     * @param int    $offset     <p>The first position used in str.</p>
11316
     * @param int    $length     [optional] <p>The maximum length of the returned string.</p>
11317
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11318
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11319
     *
11320
     * @psalm-pure
11321
     *
11322
     * @return false|string
11323
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
11324
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
11325
     *                      characters long, <b>FALSE</b> will be returned.
11326
     */
11327 172
    public static function substr(
11328
        string $str,
11329
        int $offset = 0,
11330
        int $length = null,
11331
        string $encoding = 'UTF-8',
11332
        bool $clean_utf8 = false
11333
    ) {
11334
        // empty string
11335 172
        if ($str === '' || $length === 0) {
11336 8
            return '';
11337
        }
11338
11339 168
        if ($clean_utf8) {
11340
            // iconv and mbstring are not tolerant to invalid encoding
11341
            // further, their behaviour is inconsistent with that of PHP's substr
11342 2
            $str = self::clean($str);
11343
        }
11344
11345
        // whole string
11346 168
        if (!$offset && $length === null) {
11347 7
            return $str;
11348
        }
11349
11350 163
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
11351 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11352
        }
11353
11354
        //
11355
        // fallback via mbstring
11356
        //
11357
11358 163
        if (self::$SUPPORT['mbstring'] === true && $encoding === 'UTF-8') {
11359 161
            if ($length === null) {
11360 64
                return \mb_substr($str, $offset);
11361
            }
11362
11363 102
            return \mb_substr($str, $offset, $length);
11364
        }
11365
11366
        //
11367
        // fallback for binary || ascii only
11368
        //
11369
11370
        if (
11371 4
            $encoding === 'CP850'
11372
            ||
11373 4
            $encoding === 'ASCII'
11374
        ) {
11375
            if ($length === null) {
11376
                return \substr($str, $offset);
11377
            }
11378
11379
            return \substr($str, $offset, $length);
11380
        }
11381
11382
        // otherwise we need the string-length
11383 4
        $str_length = 0;
11384 4
        if ($offset || $length === null) {
11385 4
            $str_length = self::strlen($str, $encoding);
11386
        }
11387
11388
        // e.g.: invalid chars + mbstring not installed
11389 4
        if ($str_length === false) {
11390
            return false;
11391
        }
11392
11393
        // empty string
11394 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...
11395
            return '';
11396
        }
11397
11398
        // impossible
11399 4
        if ($offset && $offset > $str_length) {
11400
            return '';
11401
        }
11402
11403 4
        if ($length === null) {
11404 4
            $length = (int) $str_length;
11405
        } else {
11406 2
            $length = (int) $length;
11407
        }
11408
11409
        if (
11410 4
            $encoding !== 'UTF-8'
11411
            &&
11412 4
            self::$SUPPORT['mbstring'] === false
11413
        ) {
11414
            /**
11415
             * @psalm-suppress ImpureFunctionCall - is is only a warning
11416
             */
11417 2
            \trigger_error('UTF8::substr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
11418
        }
11419
11420
        //
11421
        // fallback via intl
11422
        //
11423
11424
        if (
11425 4
            $encoding === 'UTF-8' // INFO: "grapheme_substr()" can't handle other encodings
11426
            &&
11427 4
            $offset >= 0 // grapheme_substr() can't handle negative offset
11428
            &&
11429 4
            self::$SUPPORT['intl'] === true
11430
        ) {
11431
            $return_tmp = \grapheme_substr($str, $offset, $length);
11432
            if ($return_tmp !== false) {
11433
                return $return_tmp;
11434
            }
11435
        }
11436
11437
        //
11438
        // fallback via iconv
11439
        //
11440
11441
        if (
11442 4
            $length >= 0 // "iconv_substr()" can't handle negative length
11443
            &&
11444 4
            self::$SUPPORT['iconv'] === true
11445
        ) {
11446
            $return_tmp = \iconv_substr($str, $offset, $length);
11447
            if ($return_tmp !== false) {
11448
                return $return_tmp;
11449
            }
11450
        }
11451
11452
        //
11453
        // fallback for ascii only
11454
        //
11455
11456 4
        if (ASCII::is_ascii($str)) {
11457
            return \substr($str, $offset, $length);
11458
        }
11459
11460
        //
11461
        // fallback via vanilla php
11462
        //
11463
11464
        // split to array, and remove invalid characters
11465 4
        $array = self::str_split($str);
11466
11467
        // extract relevant part, and join to make sting again
11468 4
        return \implode('', \array_slice($array, $offset, $length));
11469
    }
11470
11471
    /**
11472
     * Binary-safe comparison of two strings from an offset, up to a length of characters.
11473
     *
11474
     * @param string   $str1               <p>The main string being compared.</p>
11475
     * @param string   $str2               <p>The secondary string being compared.</p>
11476
     * @param int      $offset             [optional] <p>The start position for the comparison. If negative, it starts
11477
     *                                     counting from the end of the string.</p>
11478
     * @param int|null $length             [optional] <p>The length of the comparison. The default value is the largest
11479
     *                                     of the length of the str compared to the length of main_str less the
11480
     *                                     offset.</p>
11481
     * @param bool     $case_insensitivity [optional] <p>If case_insensitivity is TRUE, comparison is case
11482
     *                                     insensitive.</p>
11483
     * @param string   $encoding           [optional] <p>Set the charset for e.g. "mb_" function</p>
11484
     *
11485
     * @psalm-pure
11486
     *
11487
     * @return int
11488
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
11489
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
11490
     *             <strong>0</strong> if they are equal
11491
     */
11492 2
    public static function substr_compare(
11493
        string $str1,
11494
        string $str2,
11495
        int $offset = 0,
11496
        int $length = null,
11497
        bool $case_insensitivity = false,
11498
        string $encoding = 'UTF-8'
11499
    ): int {
11500
        if (
11501 2
            $offset !== 0
11502
            ||
11503 2
            $length !== null
11504
        ) {
11505 2
            if ($encoding === 'UTF-8') {
11506 2
                if ($length === null) {
11507 2
                    $str1 = (string) \mb_substr($str1, $offset);
11508
                } else {
11509 2
                    $str1 = (string) \mb_substr($str1, $offset, $length);
11510
                }
11511 2
                $str2 = (string) \mb_substr($str2, 0, (int) self::strlen($str1));
11512
            } else {
11513
                $encoding = self::normalize_encoding($encoding, 'UTF-8');
11514
11515
                $str1 = (string) self::substr($str1, $offset, $length, $encoding);
11516
                $str2 = (string) self::substr($str2, 0, (int) self::strlen($str1), $encoding);
11517
            }
11518
        }
11519
11520 2
        if ($case_insensitivity) {
11521 2
            return self::strcasecmp($str1, $str2, $encoding);
11522
        }
11523
11524 2
        return self::strcmp($str1, $str2);
11525
    }
11526
11527
    /**
11528
     * Count the number of substring occurrences.
11529
     *
11530
     * @see http://php.net/manual/en/function.substr-count.php
11531
     *
11532
     * @param string $haystack   <p>The string to search in.</p>
11533
     * @param string $needle     <p>The substring to search for.</p>
11534
     * @param int    $offset     [optional] <p>The offset where to start counting.</p>
11535
     * @param int    $length     [optional] <p>
11536
     *                           The maximum length after the specified offset to search for the
11537
     *                           substring. It outputs a warning if the offset plus the length is
11538
     *                           greater than the haystack length.
11539
     *                           </p>
11540
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11541
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11542
     *
11543
     * @psalm-pure
11544
     *
11545
     * @return false|int
11546
     *                   <p>This functions returns an integer or false if there isn't a string.</p>
11547
     */
11548 5
    public static function substr_count(
11549
        string $haystack,
11550
        string $needle,
11551
        int $offset = 0,
11552
        int $length = null,
11553
        string $encoding = 'UTF-8',
11554
        bool $clean_utf8 = false
11555
    ) {
11556 5
        if ($haystack === '' || $needle === '') {
11557 2
            return false;
11558
        }
11559
11560 5
        if ($length === 0) {
11561 2
            return 0;
11562
        }
11563
11564 5
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
11565 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11566
        }
11567
11568 5
        if ($clean_utf8) {
11569
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11570
            // if invalid characters are found in $haystack before $needle
11571
            $needle = self::clean($needle);
11572
            $haystack = self::clean($haystack);
11573
        }
11574
11575 5
        if ($offset || $length > 0) {
11576 2
            if ($length === null) {
11577 2
                $length_tmp = self::strlen($haystack, $encoding);
11578 2
                if ($length_tmp === false) {
11579
                    return false;
11580
                }
11581 2
                $length = (int) $length_tmp;
11582
            }
11583
11584 2
            if ($encoding === 'UTF-8') {
11585 2
                $haystack = (string) \mb_substr($haystack, $offset, $length);
11586
            } else {
11587 2
                $haystack = (string) \mb_substr($haystack, $offset, $length, $encoding);
11588
            }
11589
        }
11590
11591
        if (
11592 5
            $encoding !== 'UTF-8'
11593
            &&
11594 5
            self::$SUPPORT['mbstring'] === false
11595
        ) {
11596
            /**
11597
             * @psalm-suppress ImpureFunctionCall - is is only a warning
11598
             */
11599
            \trigger_error('UTF8::substr_count() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
11600
        }
11601
11602 5
        if (self::$SUPPORT['mbstring'] === true) {
11603 5
            if ($encoding === 'UTF-8') {
11604 5
                return \mb_substr_count($haystack, $needle);
11605
            }
11606
11607 2
            return \mb_substr_count($haystack, $needle, $encoding);
11608
        }
11609
11610
        \preg_match_all('/' . \preg_quote($needle, '/') . '/us', $haystack, $matches, \PREG_SET_ORDER);
11611
11612
        return \count($matches);
11613
    }
11614
11615
    /**
11616
     * Count the number of substring occurrences.
11617
     *
11618
     * @param string $haystack <p>
11619
     *                         The string being checked.
11620
     *                         </p>
11621
     * @param string $needle   <p>
11622
     *                         The string being found.
11623
     *                         </p>
11624
     * @param int    $offset   [optional] <p>
11625
     *                         The offset where to start counting
11626
     *                         </p>
11627
     * @param int    $length   [optional] <p>
11628
     *                         The maximum length after the specified offset to search for the
11629
     *                         substring. It outputs a warning if the offset plus the length is
11630
     *                         greater than the haystack length.
11631
     *                         </p>
11632
     *
11633
     * @psalm-pure
11634
     *
11635
     * @return false|int
11636
     *                   <p>The number of times the
11637
     *                   needle substring occurs in the
11638
     *                   haystack string.</p>
11639
     */
11640 4
    public static function substr_count_in_byte(
11641
        string $haystack,
11642
        string $needle,
11643
        int $offset = 0,
11644
        int $length = null
11645
    ) {
11646 4
        if ($haystack === '' || $needle === '') {
11647 1
            return 0;
11648
        }
11649
11650
        if (
11651 3
            ($offset || $length !== null)
11652
            &&
11653 3
            self::$SUPPORT['mbstring_func_overload'] === true
11654
        ) {
11655
            if ($length === null) {
11656
                $length_tmp = self::strlen($haystack);
11657
                if ($length_tmp === false) {
11658
                    return false;
11659
                }
11660
                $length = (int) $length_tmp;
11661
            }
11662
11663
            if (
11664
                (
11665
                    $length !== 0
11666
                    &&
11667
                    $offset !== 0
11668
                )
11669
                &&
11670
                ($length + $offset) <= 0
11671
                &&
11672
                !Bootup::is_php('7.1') // output from "substr_count()" have changed in PHP 7.1
11673
            ) {
11674
                return false;
11675
            }
11676
11677
            /** @var false|string $haystack_tmp - needed for PhpStan (stubs error) */
11678
            $haystack_tmp = \substr($haystack, $offset, $length);
11679
            if ($haystack_tmp === false) {
11680
                $haystack_tmp = '';
11681
            }
11682
            $haystack = (string) $haystack_tmp;
11683
        }
11684
11685 3
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
11686
            // "mb_" is available if overload is used, so use it ...
11687
            return \mb_substr_count($haystack, $needle, 'CP850'); // 8-BIT
11688
        }
11689
11690 3
        if ($length === null) {
11691 3
            return \substr_count($haystack, $needle, $offset);
11692
        }
11693
11694
        return \substr_count($haystack, $needle, $offset, $length);
11695
    }
11696
11697
    /**
11698
     * Returns the number of occurrences of $substring in the given string.
11699
     * By default, the comparison is case-sensitive, but can be made insensitive
11700
     * by setting $case_sensitive to false.
11701
     *
11702
     * @param string $str            <p>The input string.</p>
11703
     * @param string $substring      <p>The substring to search for.</p>
11704
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
11705
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
11706
     *
11707
     * @psalm-pure
11708
     *
11709
     * @return int
11710
     */
11711 15
    public static function substr_count_simple(
11712
        string $str,
11713
        string $substring,
11714
        bool $case_sensitive = true,
11715
        string $encoding = 'UTF-8'
11716
    ): int {
11717 15
        if ($str === '' || $substring === '') {
11718 2
            return 0;
11719
        }
11720
11721 13
        if ($encoding === 'UTF-8') {
11722 7
            if ($case_sensitive) {
11723
                return (int) \mb_substr_count($str, $substring);
11724
            }
11725
11726 7
            return (int) \mb_substr_count(
11727 7
                \mb_strtoupper($str),
11728 7
                \mb_strtoupper($substring)
11729
            );
11730
        }
11731
11732 6
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
11733
11734 6
        if ($case_sensitive) {
11735 3
            return (int) \mb_substr_count($str, $substring, $encoding);
11736
        }
11737
11738 3
        return (int) \mb_substr_count(
11739 3
            self::strtocasefold($str, true, false, $encoding, null, false),
11740 3
            self::strtocasefold($substring, true, false, $encoding, null, false),
11741 3
            $encoding
11742
        );
11743
    }
11744
11745
    /**
11746
     * Removes a prefix ($needle) from the beginning of the string ($haystack), case-insensitive.
11747
     *
11748
     * @param string $haystack <p>The string to search in.</p>
11749
     * @param string $needle   <p>The substring to search for.</p>
11750
     *
11751
     * @psalm-pure
11752
     *
11753
     * @return string
11754
     *                <p>Return the sub-string.</p>
11755
     */
11756 2
    public static function substr_ileft(string $haystack, string $needle): string
11757
    {
11758 2
        if ($haystack === '') {
11759 2
            return '';
11760
        }
11761
11762 2
        if ($needle === '') {
11763 2
            return $haystack;
11764
        }
11765
11766 2
        if (self::str_istarts_with($haystack, $needle)) {
11767 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
11768
        }
11769
11770 2
        return $haystack;
11771
    }
11772
11773
    /**
11774
     * Get part of a string process in bytes.
11775
     *
11776
     * @param string $str    <p>The string being checked.</p>
11777
     * @param int    $offset <p>The first position used in str.</p>
11778
     * @param int    $length [optional] <p>The maximum length of the returned string.</p>
11779
     *
11780
     * @psalm-pure
11781
     *
11782
     * @return false|string
11783
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
11784
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
11785
     *                      characters long, <b>FALSE</b> will be returned.
11786
     */
11787 1
    public static function substr_in_byte(string $str, int $offset = 0, int $length = null)
11788
    {
11789
        // empty string
11790 1
        if ($str === '' || $length === 0) {
11791
            return '';
11792
        }
11793
11794
        // whole string
11795 1
        if (!$offset && $length === null) {
11796
            return $str;
11797
        }
11798
11799 1
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
11800
            // "mb_" is available if overload is used, so use it ...
11801
            return \mb_substr($str, $offset, $length, 'CP850'); // 8-BIT
11802
        }
11803
11804 1
        return \substr($str, $offset, $length ?? 2147483647);
11805
    }
11806
11807
    /**
11808
     * Removes a suffix ($needle) from the end of the string ($haystack), case-insensitive.
11809
     *
11810
     * @param string $haystack <p>The string to search in.</p>
11811
     * @param string $needle   <p>The substring to search for.</p>
11812
     *
11813
     * @psalm-pure
11814
     *
11815
     * @return string
11816
     *                <p>Return the sub-string.<p>
11817
     */
11818 2
    public static function substr_iright(string $haystack, string $needle): string
11819
    {
11820 2
        if ($haystack === '') {
11821 2
            return '';
11822
        }
11823
11824 2
        if ($needle === '') {
11825 2
            return $haystack;
11826
        }
11827
11828 2
        if (self::str_iends_with($haystack, $needle)) {
11829 2
            $haystack = (string) \mb_substr($haystack, 0, (int) self::strlen($haystack) - (int) self::strlen($needle));
11830
        }
11831
11832 2
        return $haystack;
11833
    }
11834
11835
    /**
11836
     * Removes a prefix ($needle) from the beginning of the string ($haystack).
11837
     *
11838
     * @param string $haystack <p>The string to search in.</p>
11839
     * @param string $needle   <p>The substring to search for.</p>
11840
     *
11841
     * @psalm-pure
11842
     *
11843
     * @return string
11844
     *                <p>Return the sub-string.</p>
11845
     */
11846 2
    public static function substr_left(string $haystack, string $needle): string
11847
    {
11848 2
        if ($haystack === '') {
11849 2
            return '';
11850
        }
11851
11852 2
        if ($needle === '') {
11853 2
            return $haystack;
11854
        }
11855
11856 2
        if (self::str_starts_with($haystack, $needle)) {
11857 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
11858
        }
11859
11860 2
        return $haystack;
11861
    }
11862
11863
    /**
11864
     * Replace text within a portion of a string.
11865
     *
11866
     * source: https://gist.github.com/stemar/8287074
11867
     *
11868
     * @param string|string[] $str         <p>The input string or an array of stings.</p>
11869
     * @param string|string[] $replacement <p>The replacement string or an array of stings.</p>
11870
     * @param int|int[]       $offset      <p>
11871
     *                                     If start is positive, the replacing will begin at the start'th offset
11872
     *                                     into string.
11873
     *                                     <br><br>
11874
     *                                     If start is negative, the replacing will begin at the start'th character
11875
     *                                     from the end of string.
11876
     *                                     </p>
11877
     * @param int|int[]|null  $length      [optional] <p>If given and is positive, it represents the length of the
11878
     *                                     portion of string which is to be replaced. If it is negative, it
11879
     *                                     represents the number of characters from the end of string at which to
11880
     *                                     stop replacing. If it is not given, then it will default to strlen(
11881
     *                                     string ); i.e. end the replacing at the end of string. Of course, if
11882
     *                                     length is zero then this function will have the effect of inserting
11883
     *                                     replacement into string at the given start offset.</p>
11884
     * @param string          $encoding    [optional] <p>Set the charset for e.g. "mb_" function</p>
11885
     *
11886
     * @psalm-pure
11887
     *
11888
     * @return string|string[]
11889
     *                         <p>The result string is returned. If string is an array then array is returned.</p>
11890
     */
11891 10
    public static function substr_replace(
11892
        $str,
11893
        $replacement,
11894
        $offset,
11895
        $length = null,
11896
        string $encoding = 'UTF-8'
11897
    ) {
11898 10
        if (\is_array($str)) {
11899 1
            $num = \count($str);
11900
11901
            // the replacement
11902 1
            if (\is_array($replacement)) {
11903 1
                $replacement = \array_slice($replacement, 0, $num);
11904
            } else {
11905 1
                $replacement = \array_pad([$replacement], $num, $replacement);
11906
            }
11907
11908
            // the offset
11909 1
            if (\is_array($offset)) {
11910 1
                $offset = \array_slice($offset, 0, $num);
11911 1
                foreach ($offset as &$value_tmp) {
11912 1
                    $value_tmp = (int) $value_tmp === $value_tmp ? $value_tmp : 0;
11913
                }
11914 1
                unset($value_tmp);
11915
            } else {
11916 1
                $offset = \array_pad([$offset], $num, $offset);
11917
            }
11918
11919
            // the length
11920 1
            if ($length === null) {
11921 1
                $length = \array_fill(0, $num, 0);
11922 1
            } elseif (\is_array($length)) {
11923 1
                $length = \array_slice($length, 0, $num);
11924 1
                foreach ($length as &$value_tmp_V2) {
11925 1
                    $value_tmp_V2 = (int) $value_tmp_V2 === $value_tmp_V2 ? $value_tmp_V2 : $num;
11926
                }
11927 1
                unset($value_tmp_V2);
11928
            } else {
11929 1
                $length = \array_pad([$length], $num, $length);
11930
            }
11931
11932
            // recursive call
11933 1
            return \array_map([self::class, 'substr_replace'], $str, $replacement, $offset, $length);
11934
        }
11935
11936 10
        if (\is_array($replacement)) {
11937 1
            if ($replacement !== []) {
11938 1
                $replacement = $replacement[0];
11939
            } else {
11940 1
                $replacement = '';
11941
            }
11942
        }
11943
11944
        // init
11945 10
        $str = (string) $str;
11946 10
        $replacement = (string) $replacement;
11947
11948 10
        if (\is_array($length)) {
11949
            throw new \InvalidArgumentException('Parameter "$length" can only be an array, if "$str" is also an array.');
11950
        }
11951
11952 10
        if (\is_array($offset)) {
11953
            throw new \InvalidArgumentException('Parameter "$offset" can only be an array, if "$str" is also an array.');
11954
        }
11955
11956 10
        if ($str === '') {
11957 1
            return $replacement;
11958
        }
11959
11960 9
        if (self::$SUPPORT['mbstring'] === true) {
11961 9
            $string_length = (int) self::strlen($str, $encoding);
11962
11963 9
            if ($offset < 0) {
11964 1
                $offset = (int) \max(0, $string_length + $offset);
11965 9
            } elseif ($offset > $string_length) {
11966 1
                $offset = $string_length;
11967
            }
11968
11969 9
            if ($length !== null && $length < 0) {
11970 1
                $length = (int) \max(0, $string_length - $offset + $length);
11971 9
            } elseif ($length === null || $length > $string_length) {
11972 4
                $length = $string_length;
11973
            }
11974
11975
            /** @noinspection AdditionOperationOnArraysInspection */
11976 9
            if (($offset + $length) > $string_length) {
11977 4
                $length = $string_length - $offset;
11978
            }
11979
11980
            /** @noinspection AdditionOperationOnArraysInspection */
11981 9
            return ((string) \mb_substr($str, 0, $offset, $encoding)) .
11982 9
                   $replacement .
11983 9
                   ((string) \mb_substr($str, $offset + $length, $string_length - $offset - $length, $encoding));
11984
        }
11985
11986
        //
11987
        // fallback for ascii only
11988
        //
11989
11990
        if (ASCII::is_ascii($str)) {
11991
            return ($length === null) ?
11992
                \substr_replace($str, $replacement, $offset) :
11993
                \substr_replace($str, $replacement, $offset, $length);
11994
        }
11995
11996
        //
11997
        // fallback via vanilla php
11998
        //
11999
12000
        \preg_match_all('/./us', $str, $str_matches);
12001
        \preg_match_all('/./us', $replacement, $replacement_matches);
12002
12003
        if ($length === null) {
12004
            $length_tmp = self::strlen($str, $encoding);
12005
            if ($length_tmp === false) {
12006
                // e.g.: non mbstring support + invalid chars
12007
                return '';
12008
            }
12009
            $length = (int) $length_tmp;
12010
        }
12011
12012
        \array_splice($str_matches[0], $offset, $length, $replacement_matches[0]);
12013
12014
        return \implode('', $str_matches[0]);
12015
    }
12016
12017
    /**
12018
     * Removes a suffix ($needle) from the end of the string ($haystack).
12019
     *
12020
     * @param string $haystack <p>The string to search in.</p>
12021
     * @param string $needle   <p>The substring to search for.</p>
12022
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
12023
     *
12024
     * @psalm-pure
12025
     *
12026
     * @return string
12027
     *                <p>Return the sub-string.</p>
12028
     */
12029 2
    public static function substr_right(
12030
        string $haystack,
12031
        string $needle,
12032
        string $encoding = 'UTF-8'
12033
    ): string {
12034 2
        if ($haystack === '') {
12035 2
            return '';
12036
        }
12037
12038 2
        if ($needle === '') {
12039 2
            return $haystack;
12040
        }
12041
12042
        if (
12043 2
            $encoding === 'UTF-8'
12044
            &&
12045 2
            \substr($haystack, -\strlen($needle)) === $needle
12046
        ) {
12047 2
            return (string) \mb_substr($haystack, 0, (int) \mb_strlen($haystack) - (int) \mb_strlen($needle));
12048
        }
12049
12050 2
        if (\substr($haystack, -\strlen($needle)) === $needle) {
12051
            return (string) self::substr(
12052
                $haystack,
12053
                0,
12054
                (int) self::strlen($haystack, $encoding) - (int) self::strlen($needle, $encoding),
12055
                $encoding
12056
            );
12057
        }
12058
12059 2
        return $haystack;
12060
    }
12061
12062
    /**
12063
     * Returns a case swapped version of the string.
12064
     *
12065
     * @param string $str        <p>The input string.</p>
12066
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
12067
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
12068
     *
12069
     * @psalm-pure
12070
     *
12071
     * @return string
12072
     *                <p>Each character's case swapped.</p>
12073
     */
12074 6
    public static function swapCase(string $str, string $encoding = 'UTF-8', bool $clean_utf8 = false): string
12075
    {
12076 6
        if ($str === '') {
12077 1
            return '';
12078
        }
12079
12080 6
        if ($clean_utf8) {
12081
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
12082
            // if invalid characters are found in $haystack before $needle
12083 2
            $str = self::clean($str);
12084
        }
12085
12086 6
        if ($encoding === 'UTF-8') {
12087 4
            return (string) (\mb_strtolower($str) ^ \mb_strtoupper($str) ^ $str);
12088
        }
12089
12090 4
        return (string) (self::strtolower($str, $encoding) ^ self::strtoupper($str, $encoding) ^ $str);
12091
    }
12092
12093
    /**
12094
     * Checks whether symfony-polyfills are used.
12095
     *
12096
     * @psalm-pure
12097
     *
12098
     * @return bool
12099
     *              <strong>true</strong> if in use, <strong>false</strong> otherwise
12100
     */
12101
    public static function symfony_polyfill_used(): bool
12102
    {
12103
        // init
12104
        $return = false;
12105
12106
        $return_tmp = \extension_loaded('mbstring');
12107
        if (!$return_tmp && \function_exists('mb_strlen')) {
12108
            $return = true;
12109
        }
12110
12111
        $return_tmp = \extension_loaded('iconv');
12112
        if (!$return_tmp && \function_exists('iconv')) {
12113
            $return = true;
12114
        }
12115
12116
        return $return;
12117
    }
12118
12119
    /**
12120
     * @param string $str
12121
     * @param int    $tab_length
12122
     *
12123
     * @psalm-pure
12124
     *
12125
     * @return string
12126
     */
12127 6
    public static function tabs_to_spaces(string $str, int $tab_length = 4): string
12128
    {
12129 6
        if ($tab_length === 4) {
12130 3
            $spaces = '    ';
12131 3
        } elseif ($tab_length === 2) {
12132 1
            $spaces = '  ';
12133
        } else {
12134 2
            $spaces = \str_repeat(' ', $tab_length);
12135
        }
12136
12137 6
        return \str_replace("\t", $spaces, $str);
12138
    }
12139
12140
    /**
12141
     * Converts the first character of each word in the string to uppercase
12142
     * and all other chars to lowercase.
12143
     *
12144
     * @param string      $str                           <p>The input string.</p>
12145
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
12146
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
12147
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
12148
     *                                                   tr</p>
12149
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
12150
     *                                                   -> ß</p>
12151
     *
12152
     * @psalm-pure
12153
     *
12154
     * @return string
12155
     *                <p>A string with all characters of $str being title-cased.</p>
12156
     */
12157 5
    public static function titlecase(
12158
        string $str,
12159
        string $encoding = 'UTF-8',
12160
        bool $clean_utf8 = false,
12161
        string $lang = null,
12162
        bool $try_to_keep_the_string_length = false
12163
    ): string {
12164 5
        if ($clean_utf8) {
12165
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
12166
            // if invalid characters are found in $haystack before $needle
12167
            $str = self::clean($str);
12168
        }
12169
12170
        if (
12171 5
            $lang === null
12172
            &&
12173 5
            !$try_to_keep_the_string_length
12174
        ) {
12175 5
            if ($encoding === 'UTF-8') {
12176 3
                return \mb_convert_case($str, \MB_CASE_TITLE);
12177
            }
12178
12179 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
12180
12181 2
            return \mb_convert_case($str, \MB_CASE_TITLE, $encoding);
12182
        }
12183
12184
        return self::str_titleize(
12185
            $str,
12186
            null,
12187
            $encoding,
12188
            false,
12189
            $lang,
12190
            $try_to_keep_the_string_length,
12191
            false
12192
        );
12193
    }
12194
12195
    /**
12196
     * alias for "UTF8::to_ascii()"
12197
     *
12198
     * @param string $str
12199
     * @param string $subst_chr
12200
     * @param bool   $strict
12201
     *
12202
     * @psalm-pure
12203
     *
12204
     * @return string
12205
     *
12206
     * @see        UTF8::to_ascii()
12207
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
12208
     */
12209 7
    public static function toAscii(
12210
        string $str,
12211
        string $subst_chr = '?',
12212
        bool $strict = false
12213
    ): string {
12214 7
        return self::to_ascii($str, $subst_chr, $strict);
12215
    }
12216
12217
    /**
12218
     * alias for "UTF8::to_iso8859()"
12219
     *
12220
     * @param string|string[] $str
12221
     *
12222
     * @psalm-pure
12223
     *
12224
     * @return string|string[]
12225
     *
12226
     * @see        UTF8::to_iso8859()
12227
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
12228
     */
12229 2
    public static function toIso8859($str)
12230
    {
12231 2
        return self::to_iso8859($str);
12232
    }
12233
12234
    /**
12235
     * alias for "UTF8::to_latin1()"
12236
     *
12237
     * @param string|string[] $str
12238
     *
12239
     * @psalm-pure
12240
     *
12241
     * @return string|string[]
12242
     *
12243
     * @see        UTF8::to_iso8859()
12244
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
12245
     */
12246 2
    public static function toLatin1($str)
12247
    {
12248 2
        return self::to_iso8859($str);
12249
    }
12250
12251
    /**
12252
     * alias for "UTF8::to_utf8()"
12253
     *
12254
     * @param string|string[] $str
12255
     *
12256
     * @psalm-pure
12257
     *
12258
     * @return string|string[]
12259
     *
12260
     * @see        UTF8::to_utf8()
12261
     * @deprecated <p>please use "UTF8::to_utf8()"</p>
12262
     */
12263 2
    public static function toUTF8($str)
12264
    {
12265 2
        return self::to_utf8($str);
12266
    }
12267
12268
    /**
12269
     * Convert a string into ASCII.
12270
     *
12271
     * @param string $str     <p>The input string.</p>
12272
     * @param string $unknown [optional] <p>Character use if character unknown. (default is ?)</p>
12273
     * @param bool   $strict  [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad
12274
     *                        performance</p>
12275
     *
12276
     * @psalm-pure
12277
     *
12278
     * @return string
12279
     */
12280 37
    public static function to_ascii(
12281
        string $str,
12282
        string $unknown = '?',
12283
        bool $strict = false
12284
    ): string {
12285 37
        return ASCII::to_transliterate($str, $unknown, $strict);
12286
    }
12287
12288
    /**
12289
     * @param mixed $str
12290
     *
12291
     * @psalm-pure
12292
     *
12293
     * @return bool
12294
     */
12295 19
    public static function to_boolean($str): bool
12296
    {
12297
        // init
12298 19
        $str = (string) $str;
12299
12300 19
        if ($str === '') {
12301 2
            return false;
12302
        }
12303
12304
        // Info: http://php.net/manual/en/filter.filters.validate.php
12305
        $map = [
12306 17
            'true'  => true,
12307
            '1'     => true,
12308
            'on'    => true,
12309
            'yes'   => true,
12310
            'false' => false,
12311
            '0'     => false,
12312
            'off'   => false,
12313
            'no'    => false,
12314
        ];
12315
12316 17
        if (isset($map[$str])) {
12317 11
            return $map[$str];
12318
        }
12319
12320 6
        $key = \strtolower($str);
12321 6
        if (isset($map[$key])) {
12322 2
            return $map[$key];
12323
        }
12324
12325 4
        if (\is_numeric($str)) {
12326 2
            return ((float) $str + 0) > 0;
12327
        }
12328
12329 2
        return (bool) \trim($str);
12330
    }
12331
12332
    /**
12333
     * Convert given string to safe filename (and keep string case).
12334
     *
12335
     * @param string $str
12336
     * @param bool   $use_transliterate No transliteration, conversion etc. is done by default - unsafe characters are
12337
     *                                  simply replaced with hyphen.
12338
     * @param string $fallback_char
12339
     *
12340
     * @psalm-pure
12341
     *
12342
     * @return string
12343
     */
12344 1
    public static function to_filename(
12345
        string $str,
12346
        bool $use_transliterate = false,
12347
        string $fallback_char = '-'
12348
    ): string {
12349 1
        return ASCII::to_filename(
12350 1
            $str,
12351 1
            $use_transliterate,
12352 1
            $fallback_char
12353
        );
12354
    }
12355
12356
    /**
12357
     * Convert a string into "ISO-8859"-encoding (Latin-1).
12358
     *
12359
     * @param string|string[] $str
12360
     *
12361
     * @psalm-pure
12362
     *
12363
     * @return string|string[]
12364
     */
12365 8
    public static function to_iso8859($str)
12366
    {
12367 8
        if (\is_array($str)) {
12368 2
            foreach ($str as $k => &$v) {
12369 2
                $v = self::to_iso8859($v);
12370
            }
12371
12372 2
            return $str;
12373
        }
12374
12375 8
        $str = (string) $str;
12376 8
        if ($str === '') {
12377 2
            return '';
12378
        }
12379
12380 8
        return self::utf8_decode($str);
12381
    }
12382
12383
    /**
12384
     * alias for "UTF8::to_iso8859()"
12385
     *
12386
     * @param string|string[] $str
12387
     *
12388
     * @psalm-pure
12389
     *
12390
     * @return string|string[]
12391
     *
12392
     * @see        UTF8::to_iso8859()
12393
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
12394
     */
12395 2
    public static function to_latin1($str)
12396
    {
12397 2
        return self::to_iso8859($str);
12398
    }
12399
12400
    /**
12401
     * This function leaves UTF-8 characters alone, while converting almost all non-UTF8 to UTF8.
12402
     *
12403
     * <ul>
12404
     * <li>It decode UTF-8 codepoints and Unicode escape sequences.</li>
12405
     * <li>It assumes that the encoding of the original string is either WINDOWS-1252 or ISO-8859.</li>
12406
     * <li>WARNING: It does not remove invalid UTF-8 characters, so you maybe need to use "UTF8::clean()" for this
12407
     * case.</li>
12408
     * </ul>
12409
     *
12410
     * @param string|string[] $str                        <p>Any string or array.</p>
12411
     * @param bool            $decode_html_entity_to_utf8 <p>Set to true, if you need to decode html-entities.</p>
12412
     *
12413
     * @psalm-pure
12414
     *
12415
     * @return string|string[]
12416
     *                         <p>The UTF-8 encoded string</p>
12417
     *
12418
     * @noinspection SuspiciousBinaryOperationInspection
12419
     */
12420 43
    public static function to_utf8($str, bool $decode_html_entity_to_utf8 = false)
12421
    {
12422 43
        if (\is_array($str)) {
12423 4
            foreach ($str as $k => &$v) {
12424 4
                $v = self::to_utf8($v, $decode_html_entity_to_utf8);
12425
            }
12426
12427 4
            return $str;
12428
        }
12429
12430 43
        $str = (string) $str;
12431 43
        if ($str === '') {
12432 7
            return $str;
12433
        }
12434
12435 43
        $max = \strlen($str);
12436 43
        $buf = '';
12437
12438 43
        for ($i = 0; $i < $max; ++$i) {
12439 43
            $c1 = $str[$i];
12440
12441 43
            if ($c1 >= "\xC0") { // should be converted to UTF8, if it's not UTF8 already
12442
12443 39
                if ($c1 <= "\xDF") { // looks like 2 bytes UTF8
12444
12445 36
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
12446
12447 36
                    if ($c2 >= "\x80" && $c2 <= "\xBF") { // yeah, almost sure it's UTF8 already
12448 22
                        $buf .= $c1 . $c2;
12449 22
                        ++$i;
12450
                    } else { // not valid UTF8 - convert it
12451 36
                        $buf .= self::to_utf8_convert_helper($c1);
12452
                    }
12453 36
                } elseif ($c1 >= "\xE0" && $c1 <= "\xEF") { // looks like 3 bytes UTF8
12454
12455 35
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
12456 35
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
12457
12458 35
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF") { // yeah, almost sure it's UTF8 already
12459 17
                        $buf .= $c1 . $c2 . $c3;
12460 17
                        $i += 2;
12461
                    } else { // not valid UTF8 - convert it
12462 35
                        $buf .= self::to_utf8_convert_helper($c1);
12463
                    }
12464 28
                } elseif ($c1 >= "\xF0" && $c1 <= "\xF7") { // looks like 4 bytes UTF8
12465
12466 28
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
12467 28
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
12468 28
                    $c4 = $i + 3 >= $max ? "\x00" : $str[$i + 3];
12469
12470 28
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF" && $c4 >= "\x80" && $c4 <= "\xBF") { // yeah, almost sure it's UTF8 already
12471 10
                        $buf .= $c1 . $c2 . $c3 . $c4;
12472 10
                        $i += 3;
12473
                    } else { // not valid UTF8 - convert it
12474 28
                        $buf .= self::to_utf8_convert_helper($c1);
12475
                    }
12476
                } else { // doesn't look like UTF8, but should be converted
12477
12478 39
                    $buf .= self::to_utf8_convert_helper($c1);
12479
                }
12480 40
            } elseif (($c1 & "\xC0") === "\x80") { // needs conversion
12481
12482 4
                $buf .= self::to_utf8_convert_helper($c1);
12483
            } else { // it doesn't need conversion
12484
12485 40
                $buf .= $c1;
12486
            }
12487
        }
12488
12489
        // decode unicode escape sequences + unicode surrogate pairs
12490 43
        $buf = \preg_replace_callback(
12491 43
            '/\\\\u([dD][89abAB][0-9a-fA-F]{2})\\\\u([dD][cdefCDEF][\da-fA-F]{2})|\\\\u([0-9a-fA-F]{4})/',
12492
            /**
12493
             * @param array $matches
12494
             *
12495
             * @psalm-pure
12496
             *
12497
             * @return string
12498
             */
12499
            static function (array $matches): string {
12500 13
                if (isset($matches[3])) {
12501 13
                    $cp = (int) \hexdec($matches[3]);
12502
                } else {
12503
                    // http://unicode.org/faq/utf_bom.html#utf16-4
12504 1
                    $cp = ((int) \hexdec($matches[1]) << 10)
12505 1
                          + (int) \hexdec($matches[2])
12506 1
                          + 0x10000
12507 1
                          - (0xD800 << 10)
12508 1
                          - 0xDC00;
12509
                }
12510
12511
                // https://github.com/php/php-src/blob/php-7.3.2/ext/standard/html.c#L471
12512
                //
12513
                // php_utf32_utf8(unsigned char *buf, unsigned k)
12514
12515 13
                if ($cp < 0x80) {
12516 8
                    return (string) self::chr($cp);
12517
                }
12518
12519 10
                if ($cp < 0xA0) {
12520
                    /** @noinspection UnnecessaryCastingInspection */
12521
                    return (string) self::chr(0xC0 | $cp >> 6) . (string) self::chr(0x80 | $cp & 0x3F);
12522
                }
12523
12524 10
                return self::decimal_to_chr($cp);
12525 43
            },
12526 43
            $buf
12527
        );
12528
12529 43
        if ($buf === null) {
12530
            return '';
12531
        }
12532
12533
        // decode UTF-8 codepoints
12534 43
        if ($decode_html_entity_to_utf8) {
12535 3
            $buf = self::html_entity_decode($buf);
12536
        }
12537
12538 43
        return $buf;
12539
    }
12540
12541
    /**
12542
     * Returns the given string as an integer, or null if the string isn't numeric.
12543
     *
12544
     * @param string $str
12545
     *
12546
     * @psalm-pure
12547
     *
12548
     * @return int|null
12549
     *                  <p>null if the string isn't numeric</p>
12550
     */
12551 1
    public static function to_int(string $str)
12552
    {
12553 1
        if (\is_numeric($str)) {
12554 1
            return (int) $str;
12555
        }
12556
12557 1
        return null;
12558
    }
12559
12560
    /**
12561
     * Returns the given input as string, or null if the input isn't int|float|string
12562
     * and do not implement the "__toString()" method.
12563
     *
12564
     * @param mixed $input
12565
     *
12566
     * @psalm-pure
12567
     *
12568
     * @return string|null
12569
     *                     <p>null if the input isn't int|float|string and has no "__toString()" method</p>
12570
     */
12571 1
    public static function to_string($input)
12572
    {
12573
        /** @var string $input_type - hack for psalm */
12574 1
        $input_type = \gettype($input);
12575
12576
        if (
12577 1
            $input_type === 'string'
12578
            ||
12579 1
            $input_type === 'integer'
12580
            ||
12581 1
            $input_type === 'float'
12582
            ||
12583 1
            $input_type === 'double'
12584
        ) {
12585 1
            return (string) $input;
12586
        }
12587
12588
        if (
12589 1
            $input_type === 'object'
12590
            &&
12591 1
            \method_exists($input, '__toString')
12592
        ) {
12593 1
            return (string) $input;
12594
        }
12595
12596 1
        return null;
12597
    }
12598
12599
    /**
12600
     * Strip whitespace or other characters from the beginning and end of a UTF-8 string.
12601
     *
12602
     * INFO: This is slower then "trim()"
12603
     *
12604
     * We can only use the original-function, if we use <= 7-Bit in the string / chars
12605
     * but the check for ASCII (7-Bit) cost more time, then we can safe here.
12606
     *
12607
     * @param string      $str   <p>The string to be trimmed</p>
12608
     * @param string|null $chars [optional] <p>Optional characters to be stripped</p>
12609
     *
12610
     * @psalm-pure
12611
     *
12612
     * @return string
12613
     *                <p>The trimmed string.</p>
12614
     */
12615 56
    public static function trim(string $str = '', string $chars = null): string
12616
    {
12617 56
        if ($str === '') {
12618 9
            return '';
12619
        }
12620
12621 49
        if (self::$SUPPORT['mbstring'] === true) {
12622 49
            if ($chars) {
12623
                /** @noinspection PregQuoteUsageInspection */
12624 27
                $chars = \preg_quote($chars);
12625 27
                $pattern = "^[${chars}]+|[${chars}]+\$";
12626
            } else {
12627 22
                $pattern = '^[\\s]+|[\\s]+$';
12628
            }
12629
12630
            /** @noinspection PhpComposerExtensionStubsInspection */
12631 49
            return (string) \mb_ereg_replace($pattern, '', $str);
12632
        }
12633
12634 8
        if ($chars) {
12635
            $chars = \preg_quote($chars, '/');
12636
            $pattern = "^[${chars}]+|[${chars}]+\$";
12637
        } else {
12638 8
            $pattern = '^[\\s]+|[\\s]+$';
12639
        }
12640
12641 8
        return self::regex_replace($str, $pattern, '', '', '/');
12642
    }
12643
12644
    /**
12645
     * Makes string's first char uppercase.
12646
     *
12647
     * @param string      $str                           <p>The input string.</p>
12648
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
12649
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
12650
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
12651
     *                                                   tr</p>
12652
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
12653
     *                                                   -> ß</p>
12654
     *
12655
     * @psalm-pure
12656
     *
12657
     * @return string
12658
     *                <p>The resulting string with with char uppercase.</p>
12659
     */
12660 69
    public static function ucfirst(
12661
        string $str,
12662
        string $encoding = 'UTF-8',
12663
        bool $clean_utf8 = false,
12664
        string $lang = null,
12665
        bool $try_to_keep_the_string_length = false
12666
    ): string {
12667 69
        if ($str === '') {
12668 3
            return '';
12669
        }
12670
12671 68
        if ($clean_utf8) {
12672
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
12673
            // if invalid characters are found in $haystack before $needle
12674 1
            $str = self::clean($str);
12675
        }
12676
12677 68
        $use_mb_functions = $lang === null && !$try_to_keep_the_string_length;
12678
12679 68
        if ($encoding === 'UTF-8') {
12680 22
            $str_part_two = (string) \mb_substr($str, 1);
12681
12682 22
            if ($use_mb_functions) {
12683 22
                $str_part_one = \mb_strtoupper(
12684 22
                    (string) \mb_substr($str, 0, 1)
12685
                );
12686
            } else {
12687
                $str_part_one = self::strtoupper(
12688
                    (string) \mb_substr($str, 0, 1),
12689
                    $encoding,
12690
                    false,
12691
                    $lang,
12692 22
                    $try_to_keep_the_string_length
12693
                );
12694
            }
12695
        } else {
12696 47
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
12697
12698 47
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
12699
12700 47
            if ($use_mb_functions) {
12701 47
                $str_part_one = \mb_strtoupper(
12702 47
                    (string) \mb_substr($str, 0, 1, $encoding),
12703 47
                    $encoding
12704
                );
12705
            } else {
12706
                $str_part_one = self::strtoupper(
12707
                    (string) self::substr($str, 0, 1, $encoding),
12708
                    $encoding,
12709
                    false,
12710
                    $lang,
12711
                    $try_to_keep_the_string_length
12712
                );
12713
            }
12714
        }
12715
12716 68
        return $str_part_one . $str_part_two;
12717
    }
12718
12719
    /**
12720
     * alias for "UTF8::ucfirst()"
12721
     *
12722
     * @param string $str
12723
     * @param string $encoding
12724
     * @param bool   $clean_utf8
12725
     *
12726
     * @psalm-pure
12727
     *
12728
     * @return string
12729
     *
12730
     * @see        UTF8::ucfirst()
12731
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
12732
     */
12733 1
    public static function ucword(
12734
        string $str,
12735
        string $encoding = 'UTF-8',
12736
        bool $clean_utf8 = false
12737
    ): string {
12738 1
        return self::ucfirst($str, $encoding, $clean_utf8);
12739
    }
12740
12741
    /**
12742
     * Uppercase for all words in the string.
12743
     *
12744
     * @param string   $str        <p>The input string.</p>
12745
     * @param string[] $exceptions [optional] <p>Exclusion for some words.</p>
12746
     * @param string   $char_list  [optional] <p>Additional chars that contains to words and do not start a new
12747
     *                             word.</p>
12748
     * @param string   $encoding   [optional] <p>Set the charset.</p>
12749
     * @param bool     $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
12750
     *
12751
     * @psalm-pure
12752
     *
12753
     * @return string
12754
     */
12755 8
    public static function ucwords(
12756
        string $str,
12757
        array $exceptions = [],
12758
        string $char_list = '',
12759
        string $encoding = 'UTF-8',
12760
        bool $clean_utf8 = false
12761
    ): string {
12762 8
        if (!$str) {
12763 2
            return '';
12764
        }
12765
12766
        // INFO: mb_convert_case($str, MB_CASE_TITLE);
12767
        // -> MB_CASE_TITLE didn't only uppercase the first letter, it also lowercase all other letters
12768
12769 7
        if ($clean_utf8) {
12770
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
12771
            // if invalid characters are found in $haystack before $needle
12772 1
            $str = self::clean($str);
12773
        }
12774
12775 7
        $use_php_default_functions = !(bool) ($char_list . \implode('', $exceptions));
12776
12777
        if (
12778 7
            $use_php_default_functions
12779
            &&
12780 7
            ASCII::is_ascii($str)
12781
        ) {
12782
            return \ucwords($str);
12783
        }
12784
12785 7
        $words = self::str_to_words($str, $char_list);
12786 7
        $use_exceptions = $exceptions !== [];
12787
12788 7
        $words_str = '';
12789 7
        foreach ($words as &$word) {
12790 7
            if (!$word) {
12791 7
                continue;
12792
            }
12793
12794
            if (
12795 7
                !$use_exceptions
12796
                ||
12797 7
                !\in_array($word, $exceptions, true)
12798
            ) {
12799 7
                $words_str .= self::ucfirst($word, $encoding);
12800
            } else {
12801 7
                $words_str .= $word;
12802
            }
12803
        }
12804
12805 7
        return $words_str;
12806
    }
12807
12808
    /**
12809
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
12810
     *
12811
     * e.g:
12812
     * 'test+test'                     => 'test test'
12813
     * 'D&#252;sseldorf'               => 'Düsseldorf'
12814
     * 'D%FCsseldorf'                  => 'Düsseldorf'
12815
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
12816
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
12817
     * 'Düsseldorf'                   => 'Düsseldorf'
12818
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
12819
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
12820
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
12821
     *
12822
     * @param string $str          <p>The input string.</p>
12823
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
12824
     *
12825
     * @psalm-pure
12826
     *
12827
     * @return string
12828
     */
12829 4
    public static function urldecode(string $str, bool $multi_decode = true): string
12830
    {
12831 4
        if ($str === '') {
12832 3
            return '';
12833
        }
12834
12835
        if (
12836 4
            \strpos($str, '&') === false
12837
            &&
12838 4
            \strpos($str, '%') === false
12839
            &&
12840 4
            \strpos($str, '+') === false
12841
            &&
12842 4
            \strpos($str, '\u') === false
12843
        ) {
12844 3
            return self::fix_simple_utf8($str);
12845
        }
12846
12847 4
        $str = self::urldecode_unicode_helper($str);
12848
12849 4
        if ($multi_decode) {
12850
            do {
12851 3
                $str_compare = $str;
12852
12853
                /**
12854
                 * @psalm-suppress PossiblyInvalidArgument
12855
                 */
12856 3
                $str = self::fix_simple_utf8(
12857 3
                    \urldecode(
12858 3
                        self::html_entity_decode(
12859 3
                            self::to_utf8($str),
12860 3
                            \ENT_QUOTES | \ENT_HTML5
12861
                        )
12862
                    )
12863
                );
12864 3
            } while ($str_compare !== $str);
12865
        } else {
12866
            /**
12867
             * @psalm-suppress PossiblyInvalidArgument
12868
             */
12869 1
            $str = self::fix_simple_utf8(
12870 1
                \urldecode(
12871 1
                    self::html_entity_decode(
12872 1
                        self::to_utf8($str),
12873 1
                        \ENT_QUOTES | \ENT_HTML5
12874
                    )
12875
                )
12876
            );
12877
        }
12878
12879 4
        return $str;
12880
    }
12881
12882
    /**
12883
     * Return a array with "urlencoded"-win1252 -> UTF-8
12884
     *
12885
     * @psalm-pure
12886
     *
12887
     * @return string[]
12888
     *
12889
     * @deprecated <p>please use the "UTF8::urldecode()" function to decode a string</p>
12890
     */
12891 2
    public static function urldecode_fix_win1252_chars(): array
12892
    {
12893
        return [
12894 2
            '%20' => ' ',
12895
            '%21' => '!',
12896
            '%22' => '"',
12897
            '%23' => '#',
12898
            '%24' => '$',
12899
            '%25' => '%',
12900
            '%26' => '&',
12901
            '%27' => "'",
12902
            '%28' => '(',
12903
            '%29' => ')',
12904
            '%2A' => '*',
12905
            '%2B' => '+',
12906
            '%2C' => ',',
12907
            '%2D' => '-',
12908
            '%2E' => '.',
12909
            '%2F' => '/',
12910
            '%30' => '0',
12911
            '%31' => '1',
12912
            '%32' => '2',
12913
            '%33' => '3',
12914
            '%34' => '4',
12915
            '%35' => '5',
12916
            '%36' => '6',
12917
            '%37' => '7',
12918
            '%38' => '8',
12919
            '%39' => '9',
12920
            '%3A' => ':',
12921
            '%3B' => ';',
12922
            '%3C' => '<',
12923
            '%3D' => '=',
12924
            '%3E' => '>',
12925
            '%3F' => '?',
12926
            '%40' => '@',
12927
            '%41' => 'A',
12928
            '%42' => 'B',
12929
            '%43' => 'C',
12930
            '%44' => 'D',
12931
            '%45' => 'E',
12932
            '%46' => 'F',
12933
            '%47' => 'G',
12934
            '%48' => 'H',
12935
            '%49' => 'I',
12936
            '%4A' => 'J',
12937
            '%4B' => 'K',
12938
            '%4C' => 'L',
12939
            '%4D' => 'M',
12940
            '%4E' => 'N',
12941
            '%4F' => 'O',
12942
            '%50' => 'P',
12943
            '%51' => 'Q',
12944
            '%52' => 'R',
12945
            '%53' => 'S',
12946
            '%54' => 'T',
12947
            '%55' => 'U',
12948
            '%56' => 'V',
12949
            '%57' => 'W',
12950
            '%58' => 'X',
12951
            '%59' => 'Y',
12952
            '%5A' => 'Z',
12953
            '%5B' => '[',
12954
            '%5C' => '\\',
12955
            '%5D' => ']',
12956
            '%5E' => '^',
12957
            '%5F' => '_',
12958
            '%60' => '`',
12959
            '%61' => 'a',
12960
            '%62' => 'b',
12961
            '%63' => 'c',
12962
            '%64' => 'd',
12963
            '%65' => 'e',
12964
            '%66' => 'f',
12965
            '%67' => 'g',
12966
            '%68' => 'h',
12967
            '%69' => 'i',
12968
            '%6A' => 'j',
12969
            '%6B' => 'k',
12970
            '%6C' => 'l',
12971
            '%6D' => 'm',
12972
            '%6E' => 'n',
12973
            '%6F' => 'o',
12974
            '%70' => 'p',
12975
            '%71' => 'q',
12976
            '%72' => 'r',
12977
            '%73' => 's',
12978
            '%74' => 't',
12979
            '%75' => 'u',
12980
            '%76' => 'v',
12981
            '%77' => 'w',
12982
            '%78' => 'x',
12983
            '%79' => 'y',
12984
            '%7A' => 'z',
12985
            '%7B' => '{',
12986
            '%7C' => '|',
12987
            '%7D' => '}',
12988
            '%7E' => '~',
12989
            '%7F' => '',
12990
            '%80' => '`',
12991
            '%81' => '',
12992
            '%82' => '‚',
12993
            '%83' => 'ƒ',
12994
            '%84' => '„',
12995
            '%85' => '…',
12996
            '%86' => '†',
12997
            '%87' => '‡',
12998
            '%88' => 'ˆ',
12999
            '%89' => '‰',
13000
            '%8A' => 'Š',
13001
            '%8B' => '‹',
13002
            '%8C' => 'Œ',
13003
            '%8D' => '',
13004
            '%8E' => 'Ž',
13005
            '%8F' => '',
13006
            '%90' => '',
13007
            '%91' => '‘',
13008
            '%92' => '’',
13009
            '%93' => '“',
13010
            '%94' => '”',
13011
            '%95' => '•',
13012
            '%96' => '–',
13013
            '%97' => '—',
13014
            '%98' => '˜',
13015
            '%99' => '™',
13016
            '%9A' => 'š',
13017
            '%9B' => '›',
13018
            '%9C' => 'œ',
13019
            '%9D' => '',
13020
            '%9E' => 'ž',
13021
            '%9F' => 'Ÿ',
13022
            '%A0' => '',
13023
            '%A1' => '¡',
13024
            '%A2' => '¢',
13025
            '%A3' => '£',
13026
            '%A4' => '¤',
13027
            '%A5' => '¥',
13028
            '%A6' => '¦',
13029
            '%A7' => '§',
13030
            '%A8' => '¨',
13031
            '%A9' => '©',
13032
            '%AA' => 'ª',
13033
            '%AB' => '«',
13034
            '%AC' => '¬',
13035
            '%AD' => '',
13036
            '%AE' => '®',
13037
            '%AF' => '¯',
13038
            '%B0' => '°',
13039
            '%B1' => '±',
13040
            '%B2' => '²',
13041
            '%B3' => '³',
13042
            '%B4' => '´',
13043
            '%B5' => 'µ',
13044
            '%B6' => '¶',
13045
            '%B7' => '·',
13046
            '%B8' => '¸',
13047
            '%B9' => '¹',
13048
            '%BA' => 'º',
13049
            '%BB' => '»',
13050
            '%BC' => '¼',
13051
            '%BD' => '½',
13052
            '%BE' => '¾',
13053
            '%BF' => '¿',
13054
            '%C0' => 'À',
13055
            '%C1' => 'Á',
13056
            '%C2' => 'Â',
13057
            '%C3' => 'Ã',
13058
            '%C4' => 'Ä',
13059
            '%C5' => 'Å',
13060
            '%C6' => 'Æ',
13061
            '%C7' => 'Ç',
13062
            '%C8' => 'È',
13063
            '%C9' => 'É',
13064
            '%CA' => 'Ê',
13065
            '%CB' => 'Ë',
13066
            '%CC' => 'Ì',
13067
            '%CD' => 'Í',
13068
            '%CE' => 'Î',
13069
            '%CF' => 'Ï',
13070
            '%D0' => 'Ð',
13071
            '%D1' => 'Ñ',
13072
            '%D2' => 'Ò',
13073
            '%D3' => 'Ó',
13074
            '%D4' => 'Ô',
13075
            '%D5' => 'Õ',
13076
            '%D6' => 'Ö',
13077
            '%D7' => '×',
13078
            '%D8' => 'Ø',
13079
            '%D9' => 'Ù',
13080
            '%DA' => 'Ú',
13081
            '%DB' => 'Û',
13082
            '%DC' => 'Ü',
13083
            '%DD' => 'Ý',
13084
            '%DE' => 'Þ',
13085
            '%DF' => 'ß',
13086
            '%E0' => 'à',
13087
            '%E1' => 'á',
13088
            '%E2' => 'â',
13089
            '%E3' => 'ã',
13090
            '%E4' => 'ä',
13091
            '%E5' => 'å',
13092
            '%E6' => 'æ',
13093
            '%E7' => 'ç',
13094
            '%E8' => 'è',
13095
            '%E9' => 'é',
13096
            '%EA' => 'ê',
13097
            '%EB' => 'ë',
13098
            '%EC' => 'ì',
13099
            '%ED' => 'í',
13100
            '%EE' => 'î',
13101
            '%EF' => 'ï',
13102
            '%F0' => 'ð',
13103
            '%F1' => 'ñ',
13104
            '%F2' => 'ò',
13105
            '%F3' => 'ó',
13106
            '%F4' => 'ô',
13107
            '%F5' => 'õ',
13108
            '%F6' => 'ö',
13109
            '%F7' => '÷',
13110
            '%F8' => 'ø',
13111
            '%F9' => 'ù',
13112
            '%FA' => 'ú',
13113
            '%FB' => 'û',
13114
            '%FC' => 'ü',
13115
            '%FD' => 'ý',
13116
            '%FE' => 'þ',
13117
            '%FF' => 'ÿ',
13118
        ];
13119
    }
13120
13121
    /**
13122
     * Decodes a UTF-8 string to ISO-8859-1.
13123
     *
13124
     * @param string $str             <p>The input string.</p>
13125
     * @param bool   $keep_utf8_chars
13126
     *
13127
     * @psalm-pure
13128
     *
13129
     * @return string
13130
     *
13131
     * @noinspection SuspiciousBinaryOperationInspection
13132
     */
13133 14
    public static function utf8_decode(string $str, bool $keep_utf8_chars = false): string
13134
    {
13135 14
        if ($str === '') {
13136 6
            return '';
13137
        }
13138
13139
        // save for later comparision
13140 14
        $str_backup = $str;
13141 14
        $len = \strlen($str);
13142
13143 14
        if (self::$ORD === null) {
13144
            self::$ORD = self::getData('ord');
13145
        }
13146
13147 14
        if (self::$CHR === null) {
13148
            self::$CHR = self::getData('chr');
13149
        }
13150
13151 14
        $no_char_found = '?';
13152
        /** @noinspection ForeachInvariantsInspection */
13153 14
        for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
13154 14
            switch ($str[$i] & "\xF0") {
13155 14
                case "\xC0":
13156 13
                case "\xD0":
13157 13
                    $c = (self::$ORD[$str[$i] & "\x1F"] << 6) | self::$ORD[$str[++$i] & "\x3F"];
13158 13
                    $str[$j] = $c < 256 ? self::$CHR[$c] : $no_char_found;
13159
13160 13
                    break;
13161
13162
                /** @noinspection PhpMissingBreakStatementInspection */
13163 13
                case "\xF0":
13164
                    ++$i;
13165
13166
                // no break
13167
13168 13
                case "\xE0":
13169 11
                    $str[$j] = $no_char_found;
13170 11
                    $i += 2;
13171
13172 11
                    break;
13173
13174
                default:
13175 12
                    $str[$j] = $str[$i];
13176
            }
13177
        }
13178
13179
        /** @var false|string $return - needed for PhpStan (stubs error) */
13180 14
        $return = \substr($str, 0, $j);
13181 14
        if ($return === false) {
13182
            $return = '';
13183
        }
13184
13185
        if (
13186 14
            $keep_utf8_chars
13187
            &&
13188 14
            (int) self::strlen($return) >= (int) self::strlen($str_backup)
13189
        ) {
13190 2
            return $str_backup;
13191
        }
13192
13193 14
        return $return;
13194
    }
13195
13196
    /**
13197
     * Encodes an ISO-8859-1 string to UTF-8.
13198
     *
13199
     * @param string $str <p>The input string.</p>
13200
     *
13201
     * @psalm-pure
13202
     *
13203
     * @return string
13204
     */
13205 16
    public static function utf8_encode(string $str): string
13206
    {
13207 16
        if ($str === '') {
13208 14
            return '';
13209
        }
13210
13211
        /** @var false|string $str - the polyfill maybe return false */
13212 16
        $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

13212
        $str = \utf8_encode(/** @scrutinizer ignore-type */ $str);
Loading history...
13213
13214
        /** @noinspection CallableParameterUseCaseInTypeContextInspection */
13215
        /** @psalm-suppress TypeDoesNotContainType */
13216 16
        if ($str === false) {
13217
            return '';
13218
        }
13219
13220 16
        return $str;
13221
    }
13222
13223
    /**
13224
     * fix -> utf8-win1252 chars
13225
     *
13226
     * @param string $str <p>The input string.</p>
13227
     *
13228
     * @psalm-pure
13229
     *
13230
     * @return string
13231
     *
13232
     * @deprecated <p>please use "UTF8::fix_simple_utf8()"</p>
13233
     */
13234 2
    public static function utf8_fix_win1252_chars(string $str): string
13235
    {
13236 2
        return self::fix_simple_utf8($str);
13237
    }
13238
13239
    /**
13240
     * Returns an array with all utf8 whitespace characters.
13241
     *
13242
     * @see http://www.bogofilter.org/pipermail/bogofilter/2003-March/001889.html
13243
     *
13244
     * @psalm-pure
13245
     *
13246
     * @return string[]
13247
     *                  An array with all known whitespace characters as values and the type of whitespace as keys
13248
     *                  as defined in above URL
13249
     */
13250 2
    public static function whitespace_table(): array
13251
    {
13252 2
        return self::$WHITESPACE_TABLE;
13253
    }
13254
13255
    /**
13256
     * Limit the number of words in a string.
13257
     *
13258
     * @param string $str        <p>The input string.</p>
13259
     * @param int    $limit      <p>The limit of words as integer.</p>
13260
     * @param string $str_add_on <p>Replacement for the striped string.</p>
13261
     *
13262
     * @psalm-pure
13263
     *
13264
     * @return string
13265
     */
13266 2
    public static function words_limit(
13267
        string $str,
13268
        int $limit = 100,
13269
        string $str_add_on = '…'
13270
    ): string {
13271 2
        if ($str === '' || $limit < 1) {
13272 2
            return '';
13273
        }
13274
13275 2
        \preg_match('/^\\s*+(?:[^\\s]++\\s*+){1,' . $limit . '}/u', $str, $matches);
13276
13277
        if (
13278 2
            !isset($matches[0])
13279
            ||
13280 2
            \mb_strlen($str) === (int) \mb_strlen($matches[0])
13281
        ) {
13282 2
            return $str;
13283
        }
13284
13285 2
        return \rtrim($matches[0]) . $str_add_on;
13286
    }
13287
13288
    /**
13289
     * Wraps a string to a given number of characters
13290
     *
13291
     * @see http://php.net/manual/en/function.wordwrap.php
13292
     *
13293
     * @param string $str   <p>The input string.</p>
13294
     * @param int    $width [optional] <p>The column width.</p>
13295
     * @param string $break [optional] <p>The line is broken using the optional break parameter.</p>
13296
     * @param bool   $cut   [optional] <p>
13297
     *                      If the cut is set to true, the string is
13298
     *                      always wrapped at or before the specified width. So if you have
13299
     *                      a word that is larger than the given width, it is broken apart.
13300
     *                      </p>
13301
     *
13302
     * @psalm-pure
13303
     *
13304
     * @return string
13305
     *                <p>The given string wrapped at the specified column.</p>
13306
     */
13307 12
    public static function wordwrap(
13308
        string $str,
13309
        int $width = 75,
13310
        string $break = "\n",
13311
        bool $cut = false
13312
    ): string {
13313 12
        if ($str === '' || $break === '') {
13314 4
            return '';
13315
        }
13316
13317 10
        $str_split = \explode($break, $str);
13318 10
        if ($str_split === false) {
13319
            return '';
13320
        }
13321
13322
        /** @var string[] $charsArray */
13323 10
        $charsArray = [];
13324 10
        $word_split = '';
13325 10
        foreach ($str_split as $i => $i_value) {
13326 10
            if ($i) {
13327 3
                $charsArray[] = $break;
13328 3
                $word_split .= '#';
13329
            }
13330
13331 10
            foreach (self::str_split($i_value) as $c) {
13332 10
                $charsArray[] = $c;
13333 10
                if ($c === ' ') {
13334 3
                    $word_split .= ' ';
13335
                } else {
13336 10
                    $word_split .= '?';
13337
                }
13338
            }
13339
        }
13340
13341 10
        $str_return = '';
13342 10
        $j = 0;
13343 10
        $b = -1;
13344 10
        $i = -1;
13345 10
        $word_split = \wordwrap($word_split, $width, '#', $cut);
13346
13347 10
        $max = \mb_strlen($word_split);
13348 10
        while (($b = \mb_strpos($word_split, '#', $b + 1)) !== false) {
13349 8
            for (++$i; $i < $b; ++$i) {
13350 8
                if (isset($charsArray[$j])) {
13351 8
                    $str_return .= $charsArray[$j];
13352 8
                    unset($charsArray[$j]);
13353
                }
13354 8
                ++$j;
13355
13356
                // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
13357 8
                if ($i > $max) {
13358
                    break 2;
13359
                }
13360
            }
13361
13362
            if (
13363 8
                $break === $charsArray[$j]
13364
                ||
13365 8
                $charsArray[$j] === ' '
13366
            ) {
13367 5
                unset($charsArray[$j++]);
13368
            }
13369
13370 8
            $str_return .= $break;
13371
13372
            // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
13373 8
            if ($b > $max) {
13374
                break;
13375
            }
13376
        }
13377
13378 10
        return $str_return . \implode('', $charsArray);
13379
    }
13380
13381
    /**
13382
     * Line-Wrap the string after $limit, but split the string by "$delimiter" before ...
13383
     *    ... so that we wrap the per line.
13384
     *
13385
     * @param string      $str             <p>The input string.</p>
13386
     * @param int         $width           [optional] <p>The column width.</p>
13387
     * @param string      $break           [optional] <p>The line is broken using the optional break parameter.</p>
13388
     * @param bool        $cut             [optional] <p>
13389
     *                                     If the cut is set to true, the string is
13390
     *                                     always wrapped at or before the specified width. So if you have
13391
     *                                     a word that is larger than the given width, it is broken apart.
13392
     *                                     </p>
13393
     * @param bool        $add_final_break [optional] <p>
13394
     *                                     If this flag is true, then the method will add a $break at the end
13395
     *                                     of the result string.
13396
     *                                     </p>
13397
     * @param string|null $delimiter       [optional] <p>
13398
     *                                     You can change the default behavior, where we split the string by newline.
13399
     *                                     </p>
13400
     *
13401
     * @psalm-pure
13402
     *
13403
     * @return string
13404
     */
13405 1
    public static function wordwrap_per_line(
13406
        string $str,
13407
        int $width = 75,
13408
        string $break = "\n",
13409
        bool $cut = false,
13410
        bool $add_final_break = true,
13411
        string $delimiter = null
13412
    ): string {
13413 1
        if ($delimiter === null) {
13414 1
            $strings = \preg_split('/\\r\\n|\\r|\\n/', $str);
13415
        } else {
13416 1
            $strings = \explode($delimiter, $str);
13417
        }
13418
13419 1
        $string_helper_array = [];
13420 1
        if ($strings !== false) {
13421 1
            foreach ($strings as $value) {
13422 1
                $string_helper_array[] = self::wordwrap($value, $width, $break, $cut);
13423
            }
13424
        }
13425
13426 1
        if ($add_final_break) {
13427 1
            $final_break = $break;
13428
        } else {
13429 1
            $final_break = '';
13430
        }
13431
13432 1
        return \implode($delimiter ?? "\n", $string_helper_array) . $final_break;
13433
    }
13434
13435
    /**
13436
     * Returns an array of Unicode White Space characters.
13437
     *
13438
     * @psalm-pure
13439
     *
13440
     * @return string[]
13441
     *                  <p>An array with numeric code point as key and White Space Character as value.</p>
13442
     */
13443 2
    public static function ws(): array
13444
    {
13445 2
        return self::$WHITESPACE;
13446
    }
13447
13448
    /**
13449
     * Checks whether the passed string contains only byte sequences that are valid UTF-8 characters.
13450
     *
13451
     * @see          http://hsivonen.iki.fi/php-utf8/
13452
     *
13453
     * @param string $str    <p>The string to be checked.</p>
13454
     * @param bool   $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
13455
     *
13456
     * @psalm-pure
13457
     *
13458
     * @return bool
13459
     *
13460
     * @noinspection ReturnTypeCanBeDeclaredInspection
13461
     */
13462 108
    private static function is_utf8_string(string $str, bool $strict = false)
13463
    {
13464 108
        if ($str === '') {
13465 14
            return true;
13466
        }
13467
13468 102
        if ($strict) {
13469 2
            $is_binary = self::is_binary($str, true);
13470
13471 2
            if ($is_binary && self::is_utf16($str, false) !== false) {
13472 2
                return false;
13473
            }
13474
13475
            if ($is_binary && self::is_utf32($str, false) !== false) {
13476
                return false;
13477
            }
13478
        }
13479
13480 102
        if (!self::pcre_utf8_support()) {
13481
            // If even just the first character can be matched, when the /u
13482
            // modifier is used, then it's valid UTF-8. If the UTF-8 is somehow
13483
            // invalid, nothing at all will match, even if the string contains
13484
            // some valid sequences
13485
            return \preg_match('/^./us', $str, $ar) === 1;
13486
        }
13487
13488 102
        $mState = 0; // cached expected number of octets after the current octet
13489
        // until the beginning of the next UTF8 character sequence
13490 102
        $mUcs4 = 0; // cached Unicode character
13491 102
        $mBytes = 1; // cached expected number of octets in the current sequence
13492
13493 102
        if (self::$ORD === null) {
13494
            self::$ORD = self::getData('ord');
13495
        }
13496
13497 102
        $len = \strlen($str);
13498
        /** @noinspection ForeachInvariantsInspection */
13499 102
        for ($i = 0; $i < $len; ++$i) {
13500 102
            $in = self::$ORD[$str[$i]];
13501
13502 102
            if ($mState === 0) {
13503
                // When mState is zero we expect either a US-ASCII character or a
13504
                // multi-octet sequence.
13505 102
                if ((0x80 & $in) === 0) {
13506
                    // US-ASCII, pass straight through.
13507 97
                    $mBytes = 1;
13508 83
                } elseif ((0xE0 & $in) === 0xC0) {
13509
                    // First octet of 2 octet sequence.
13510 73
                    $mUcs4 = $in;
13511 73
                    $mUcs4 = ($mUcs4 & 0x1F) << 6;
13512 73
                    $mState = 1;
13513 73
                    $mBytes = 2;
13514 58
                } elseif ((0xF0 & $in) === 0xE0) {
13515
                    // First octet of 3 octet sequence.
13516 42
                    $mUcs4 = $in;
13517 42
                    $mUcs4 = ($mUcs4 & 0x0F) << 12;
13518 42
                    $mState = 2;
13519 42
                    $mBytes = 3;
13520 29
                } elseif ((0xF8 & $in) === 0xF0) {
13521
                    // First octet of 4 octet sequence.
13522 18
                    $mUcs4 = $in;
13523 18
                    $mUcs4 = ($mUcs4 & 0x07) << 18;
13524 18
                    $mState = 3;
13525 18
                    $mBytes = 4;
13526 13
                } elseif ((0xFC & $in) === 0xF8) {
13527
                    /* First octet of 5 octet sequence.
13528
                     *
13529
                     * This is illegal because the encoded codepoint must be either
13530
                     * (a) not the shortest form or
13531
                     * (b) outside the Unicode range of 0-0x10FFFF.
13532
                     * Rather than trying to resynchronize, we will carry on until the end
13533
                     * of the sequence and let the later error handling code catch it.
13534
                     */
13535 5
                    $mUcs4 = $in;
13536 5
                    $mUcs4 = ($mUcs4 & 0x03) << 24;
13537 5
                    $mState = 4;
13538 5
                    $mBytes = 5;
13539 10
                } elseif ((0xFE & $in) === 0xFC) {
13540
                    // First octet of 6 octet sequence, see comments for 5 octet sequence.
13541 5
                    $mUcs4 = $in;
13542 5
                    $mUcs4 = ($mUcs4 & 1) << 30;
13543 5
                    $mState = 5;
13544 5
                    $mBytes = 6;
13545
                } else {
13546
                    // Current octet is neither in the US-ASCII range nor a legal first
13547
                    // octet of a multi-octet sequence.
13548 102
                    return false;
13549
                }
13550 83
            } elseif ((0xC0 & $in) === 0x80) {
13551
13552
                // When mState is non-zero, we expect a continuation of the multi-octet
13553
                // sequence
13554
13555
                // Legal continuation.
13556 75
                $shift = ($mState - 1) * 6;
13557 75
                $tmp = $in;
13558 75
                $tmp = ($tmp & 0x0000003F) << $shift;
13559 75
                $mUcs4 |= $tmp;
13560
                // Prefix: End of the multi-octet sequence. mUcs4 now contains the final
13561
                // Unicode code point to be output.
13562 75
                if (--$mState === 0) {
13563
                    // Check for illegal sequences and code points.
13564
                    //
13565
                    // From Unicode 3.1, non-shortest form is illegal
13566
                    if (
13567 75
                        ($mBytes === 2 && $mUcs4 < 0x0080)
13568
                        ||
13569 75
                        ($mBytes === 3 && $mUcs4 < 0x0800)
13570
                        ||
13571 75
                        ($mBytes === 4 && $mUcs4 < 0x10000)
13572
                        ||
13573 75
                        ($mBytes > 4)
13574
                        ||
13575
                        // From Unicode 3.2, surrogate characters are illegal.
13576 75
                        (($mUcs4 & 0xFFFFF800) === 0xD800)
13577
                        ||
13578
                        // Code points outside the Unicode range are illegal.
13579 75
                        ($mUcs4 > 0x10FFFF)
13580
                    ) {
13581 9
                        return false;
13582
                    }
13583
                    // initialize UTF8 cache
13584 75
                    $mState = 0;
13585 75
                    $mUcs4 = 0;
13586 75
                    $mBytes = 1;
13587
                }
13588
            } else {
13589
                // ((0xC0 & (*in) != 0x80) && (mState != 0))
13590
                // Incomplete multi-octet sequence.
13591 35
                return false;
13592
            }
13593
        }
13594
13595 67
        return true;
13596
    }
13597
13598
    /**
13599
     * @param string $str
13600
     * @param bool   $use_lowercase      <p>Use uppercase by default, otherwise use lowercase.</p>
13601
     * @param bool   $use_full_case_fold <p>Convert not only common cases.</p>
13602
     *
13603
     * @psalm-pure
13604
     *
13605
     * @return string
13606
     *
13607
     * @noinspection ReturnTypeCanBeDeclaredInspection
13608
     */
13609 33
    private static function fixStrCaseHelper(
13610
        string $str,
13611
        bool $use_lowercase = false,
13612
        bool $use_full_case_fold = false
13613
    ) {
13614 33
        $upper = self::$COMMON_CASE_FOLD['upper'];
13615 33
        $lower = self::$COMMON_CASE_FOLD['lower'];
13616
13617 33
        if ($use_lowercase) {
13618 2
            $str = \str_replace(
13619 2
                $upper,
13620 2
                $lower,
13621 2
                $str
13622
            );
13623
        } else {
13624 31
            $str = \str_replace(
13625 31
                $lower,
13626 31
                $upper,
13627 31
                $str
13628
            );
13629
        }
13630
13631 33
        if ($use_full_case_fold) {
13632
            /**
13633
             * @psalm-suppress ImpureStaticVariable
13634
             *
13635
             * @var array<mixed>|null
13636
             */
13637 31
            static $FULL_CASE_FOLD = null;
13638 31
            if ($FULL_CASE_FOLD === null) {
13639 1
                $FULL_CASE_FOLD = self::getData('caseFolding_full');
13640
            }
13641
13642 31
            if ($use_lowercase) {
13643 2
                $str = \str_replace($FULL_CASE_FOLD[0], $FULL_CASE_FOLD[1], $str);
13644
            } else {
13645 29
                $str = \str_replace($FULL_CASE_FOLD[1], $FULL_CASE_FOLD[0], $str);
13646
            }
13647
        }
13648
13649 33
        return $str;
13650
    }
13651
13652
    /**
13653
     * get data from "/data/*.php"
13654
     *
13655
     * @param string $file
13656
     *
13657
     * @psalm-pure
13658
     *
13659
     * @return array
13660
     *
13661
     * @noinspection ReturnTypeCanBeDeclaredInspection
13662
     */
13663 6
    private static function getData(string $file)
13664
    {
13665
        /** @noinspection PhpIncludeInspection */
13666
        /** @noinspection UsingInclusionReturnValueInspection */
13667
        /** @psalm-suppress UnresolvableInclude */
13668 6
        return include __DIR__ . '/data/' . $file . '.php';
13669
    }
13670
13671
    /**
13672
     * @psalm-pure
13673
     *
13674
     * @return true|null
13675
     */
13676 12
    private static function initEmojiData()
13677
    {
13678 12
        if (self::$EMOJI_KEYS_CACHE === null) {
13679 1
            if (self::$EMOJI === null) {
13680 1
                self::$EMOJI = self::getData('emoji');
13681
            }
13682
13683
            /**
13684
             * @psalm-suppress ImpureFunctionCall - static sort function is used
13685
             */
13686 1
            \uksort(
13687 1
                self::$EMOJI,
13688
                static function (string $a, string $b): int {
13689 1
                    return \strlen($b) <=> \strlen($a);
13690 1
                }
13691
            );
13692
13693 1
            self::$EMOJI_KEYS_CACHE = \array_keys(self::$EMOJI);
13694 1
            self::$EMOJI_VALUES_CACHE = self::$EMOJI;
13695
13696 1
            foreach (self::$EMOJI_KEYS_CACHE as $key) {
13697 1
                $tmp_key = \crc32($key);
13698 1
                self::$EMOJI_KEYS_REVERSIBLE_CACHE[] = '_-_PORTABLE_UTF8_-_' . $tmp_key . '_-_' . \strrev((string) $tmp_key) . '_-_8FTU_ELBATROP_-_';
13699
            }
13700
13701 1
            return true;
13702
        }
13703
13704 12
        return null;
13705
    }
13706
13707
    /**
13708
     * Checks whether mbstring "overloaded" is active on the server.
13709
     *
13710
     * @psalm-pure
13711
     *
13712
     * @return bool
13713
     *
13714
     * @noinspection ReturnTypeCanBeDeclaredInspection
13715
     */
13716
    private static function mbstring_overloaded()
13717
    {
13718
        /**
13719
         * INI directive 'mbstring.func_overload' is deprecated since PHP 7.2
13720
         */
13721
13722
        /** @noinspection PhpComposerExtensionStubsInspection */
13723
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
13724
        return \defined('MB_OVERLOAD_STRING')
13725
               &&
13726
               ((int) @\ini_get('mbstring.func_overload') & \MB_OVERLOAD_STRING);
13727
    }
13728
13729
    /**
13730
     * @param array    $strings
13731
     * @param bool     $remove_empty_values
13732
     * @param int|null $remove_short_values
13733
     *
13734
     * @psalm-pure
13735
     *
13736
     * @return array
13737
     *
13738
     * @noinspection ReturnTypeCanBeDeclaredInspection
13739
     */
13740 2
    private static function reduce_string_array(
13741
        array $strings,
13742
        bool $remove_empty_values,
13743
        int $remove_short_values = null
13744
    ) {
13745
        // init
13746 2
        $return = [];
13747
13748 2
        foreach ($strings as &$str) {
13749
            if (
13750 2
                $remove_short_values !== null
13751
                &&
13752 2
                \mb_strlen($str) <= $remove_short_values
13753
            ) {
13754 2
                continue;
13755
            }
13756
13757
            if (
13758 2
                $remove_empty_values
13759
                &&
13760 2
                \trim($str) === ''
13761
            ) {
13762 2
                continue;
13763
            }
13764
13765 2
            $return[] = $str;
13766
        }
13767
13768 2
        return $return;
13769
    }
13770
13771
    /**
13772
     * rxClass
13773
     *
13774
     * @param string $s
13775
     * @param string $class
13776
     *
13777
     * @psalm-pure
13778
     *
13779
     * @return string
13780
     *
13781
     * @noinspection ReturnTypeCanBeDeclaredInspection
13782
     */
13783 33
    private static function rxClass(string $s, string $class = '')
13784
    {
13785
        /**
13786
         * @psalm-suppress ImpureStaticVariable
13787
         *
13788
         * @var array<string,string>
13789
         */
13790 33
        static $RX_CLASS_CACHE = [];
13791
13792 33
        $cache_key = $s . '_' . $class;
13793
13794 33
        if (isset($RX_CLASS_CACHE[$cache_key])) {
13795 21
            return $RX_CLASS_CACHE[$cache_key];
13796
        }
13797
13798
        /** @var string[] $class_array */
13799 16
        $class_array[] = $class;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$class_array was never initialized. Although not strictly required by PHP, it is generally a good practice to add $class_array = array(); before regardless.
Loading history...
13800
13801
        /** @noinspection SuspiciousLoopInspection */
13802
        /** @noinspection AlterInForeachInspection */
13803 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...
13804 15
            if ($s === '-') {
13805
                $class_array[0] = '-' . $class_array[0];
13806 15
            } elseif (!isset($s[2])) {
13807 15
                $class_array[0] .= \preg_quote($s, '/');
13808 1
            } elseif (self::strlen($s) === 1) {
13809 1
                $class_array[0] .= $s;
13810
            } else {
13811 15
                $class_array[] = $s;
13812
            }
13813
        }
13814
13815 16
        if ($class_array[0]) {
13816 16
            $class_array[0] = '[' . $class_array[0] . ']';
13817
        }
13818
13819 16
        if (\count($class_array) === 1) {
13820 16
            $return = $class_array[0];
13821
        } else {
13822
            $return = '(?:' . \implode('|', $class_array) . ')';
13823
        }
13824
13825 16
        $RX_CLASS_CACHE[$cache_key] = $return;
13826
13827 16
        return $return;
13828
    }
13829
13830
    /**
13831
     * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
13832
     *
13833
     * @param string $names
13834
     * @param string $delimiter
13835
     * @param string $encoding
13836
     *
13837
     * @psalm-pure
13838
     *
13839
     * @return string
13840
     *
13841
     * @noinspection ReturnTypeCanBeDeclaredInspection
13842
     */
13843 1
    private static function str_capitalize_name_helper(
13844
        string $names,
13845
        string $delimiter,
13846
        string $encoding = 'UTF-8'
13847
    ) {
13848
        // init
13849 1
        $name_helper_array = \explode($delimiter, $names);
13850 1
        if ($name_helper_array === false) {
13851
            return '';
13852
        }
13853
13854
        $special_cases = [
13855 1
            'names' => [
13856
                'ab',
13857
                'af',
13858
                'al',
13859
                'and',
13860
                'ap',
13861
                'bint',
13862
                'binte',
13863
                'da',
13864
                'de',
13865
                'del',
13866
                'den',
13867
                'der',
13868
                'di',
13869
                'dit',
13870
                'ibn',
13871
                'la',
13872
                'mac',
13873
                'nic',
13874
                'of',
13875
                'ter',
13876
                'the',
13877
                'und',
13878
                'van',
13879
                'von',
13880
                'y',
13881
                'zu',
13882
            ],
13883
            'prefixes' => [
13884
                'al-',
13885
                "d'",
13886
                'ff',
13887
                "l'",
13888
                'mac',
13889
                'mc',
13890
                'nic',
13891
            ],
13892
        ];
13893
13894 1
        foreach ($name_helper_array as &$name) {
13895 1
            if (\in_array($name, $special_cases['names'], true)) {
13896 1
                continue;
13897
            }
13898
13899 1
            $continue = false;
13900
13901 1
            if ($delimiter === '-') {
13902
                /** @noinspection AlterInForeachInspection */
13903 1
                foreach ((array) $special_cases['names'] as &$beginning) {
13904 1
                    if (self::strpos($name, $beginning, 0, $encoding) === 0) {
13905 1
                        $continue = true;
13906
13907 1
                        break;
13908
                    }
13909
                }
13910
            }
13911
13912
            /** @noinspection AlterInForeachInspection */
13913 1
            foreach ((array) $special_cases['prefixes'] as &$beginning) {
13914 1
                if (self::strpos($name, $beginning, 0, $encoding) === 0) {
13915 1
                    $continue = true;
13916
13917 1
                    break;
13918
                }
13919
            }
13920
13921 1
            if ($continue) {
13922 1
                continue;
13923
            }
13924
13925 1
            $name = self::ucfirst($name);
13926
        }
13927
13928 1
        return \implode($delimiter, $name_helper_array);
13929
    }
13930
13931
    /**
13932
     * Generic case-sensitive transformation for collation matching.
13933
     *
13934
     * @param string $str <p>The input string</p>
13935
     *
13936
     * @psalm-pure
13937
     *
13938
     * @return string|null
13939
     */
13940 6
    private static function strtonatfold(string $str)
13941
    {
13942
        /** @noinspection PhpUndefinedClassInspection */
13943 6
        return \preg_replace(
13944 6
            '/\p{Mn}+/u',
13945 6
            '',
13946 6
            \Normalizer::normalize($str, \Normalizer::NFD)
13947
        );
13948
    }
13949
13950
    /**
13951
     * @param int|string $input
13952
     *
13953
     * @psalm-pure
13954
     *
13955
     * @return string
13956
     *
13957
     * @noinspection ReturnTypeCanBeDeclaredInspection
13958
     * @noinspection SuspiciousBinaryOperationInspection
13959
     */
13960 31
    private static function to_utf8_convert_helper($input)
13961
    {
13962
        // init
13963 31
        $buf = '';
13964
13965 31
        if (self::$ORD === null) {
13966 1
            self::$ORD = self::getData('ord');
13967
        }
13968
13969 31
        if (self::$CHR === null) {
13970 1
            self::$CHR = self::getData('chr');
13971
        }
13972
13973 31
        if (self::$WIN1252_TO_UTF8 === null) {
13974 1
            self::$WIN1252_TO_UTF8 = self::getData('win1252_to_utf8');
13975
        }
13976
13977 31
        $ordC1 = self::$ORD[$input];
13978 31
        if (isset(self::$WIN1252_TO_UTF8[$ordC1])) { // found in Windows-1252 special cases
13979 31
            $buf .= self::$WIN1252_TO_UTF8[$ordC1];
13980
        } else {
13981
            /** @noinspection OffsetOperationsInspection */
13982 1
            $cc1 = self::$CHR[$ordC1 / 64] | "\xC0";
13983 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...
13984 1
            $buf .= $cc1 . $cc2;
13985
        }
13986
13987 31
        return $buf;
13988
    }
13989
13990
    /**
13991
     * @param string $str
13992
     *
13993
     * @psalm-pure
13994
     *
13995
     * @return string
13996
     *
13997
     * @noinspection ReturnTypeCanBeDeclaredInspection
13998
     */
13999 10
    private static function urldecode_unicode_helper(string $str)
14000
    {
14001 10
        $pattern = '/%u([0-9a-fA-F]{3,4})/';
14002 10
        if (\preg_match($pattern, $str)) {
14003 7
            $str = (string) \preg_replace($pattern, '&#x\\1;', $str);
14004
        }
14005
14006 10
        return $str;
14007
    }
14008
}
14009