Passed
Pull Request — master (#93)
by Graham
03:01
created

UTF8::json_encode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 10
ccs 4
cts 5
cp 0.8
crap 2.032
rs 10
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
    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}])";
17
18
    /**
19
     * Bom => Byte-Length
20
     *
21
     * INFO: https://en.wikipedia.org/wiki/Byte_order_mark
22
     *
23
     * @var array<string, int>
24
     */
25
    private static $BOM = [
26
        "\xef\xbb\xbf"     => 3, // UTF-8 BOM
27
        ''              => 6, // UTF-8 BOM as "WINDOWS-1252" (one char has [maybe] more then one byte ...)
28
        "\x00\x00\xfe\xff" => 4, // UTF-32 (BE) BOM
29
        '  þÿ'             => 6, // UTF-32 (BE) BOM as "WINDOWS-1252"
30
        "\xff\xfe\x00\x00" => 4, // UTF-32 (LE) BOM
31
        'ÿþ  '             => 6, // UTF-32 (LE) BOM as "WINDOWS-1252"
32
        "\xfe\xff"         => 2, // UTF-16 (BE) BOM
33
        'þÿ'               => 4, // UTF-16 (BE) BOM as "WINDOWS-1252"
34
        "\xff\xfe"         => 2, // UTF-16 (LE) BOM
35
        'ÿþ'               => 4, // UTF-16 (LE) BOM as "WINDOWS-1252"
36
    ];
37
38
    /**
39
     * Numeric code point => UTF-8 Character
40
     *
41
     * url: http://www.w3schools.com/charsets/ref_utf_punctuation.asp
42
     *
43
     * @var array<int, string>
44
     */
45
    private static $WHITESPACE = [
46
        // NUL Byte
47
        0 => "\x0",
48
        // Tab
49
        9 => "\x9",
50
        // New Line
51
        10 => "\xa",
52
        // Vertical Tab
53
        11 => "\xb",
54
        // Carriage Return
55
        13 => "\xd",
56
        // Ordinary Space
57
        32 => "\x20",
58
        // NO-BREAK SPACE
59
        160 => "\xc2\xa0",
60
        // OGHAM SPACE MARK
61
        5760 => "\xe1\x9a\x80",
62
        // MONGOLIAN VOWEL SEPARATOR
63
        6158 => "\xe1\xa0\x8e",
64
        // EN QUAD
65
        8192 => "\xe2\x80\x80",
66
        // EM QUAD
67
        8193 => "\xe2\x80\x81",
68
        // EN SPACE
69
        8194 => "\xe2\x80\x82",
70
        // EM SPACE
71
        8195 => "\xe2\x80\x83",
72
        // THREE-PER-EM SPACE
73
        8196 => "\xe2\x80\x84",
74
        // FOUR-PER-EM SPACE
75
        8197 => "\xe2\x80\x85",
76
        // SIX-PER-EM SPACE
77
        8198 => "\xe2\x80\x86",
78
        // FIGURE SPACE
79
        8199 => "\xe2\x80\x87",
80
        // PUNCTUATION SPACE
81
        8200 => "\xe2\x80\x88",
82
        // THIN SPACE
83
        8201 => "\xe2\x80\x89",
84
        //HAIR SPACE
85
        8202 => "\xe2\x80\x8a",
86
        // LINE SEPARATOR
87
        8232 => "\xe2\x80\xa8",
88
        // PARAGRAPH SEPARATOR
89
        8233 => "\xe2\x80\xa9",
90
        // NARROW NO-BREAK SPACE
91
        8239 => "\xe2\x80\xaf",
92
        // MEDIUM MATHEMATICAL SPACE
93
        8287 => "\xe2\x81\x9f",
94
        // HALFWIDTH HANGUL FILLER
95
        65440 => "\xef\xbe\xa0",
96
        // IDEOGRAPHIC SPACE
97
        12288 => "\xe3\x80\x80",
98
    ];
99
100
    /**
101
     * @var array<string, string>
102
     */
103
    private static $WHITESPACE_TABLE = [
104
        'SPACE'                     => "\x20",
105
        'NO-BREAK SPACE'            => "\xc2\xa0",
106
        'OGHAM SPACE MARK'          => "\xe1\x9a\x80",
107
        'EN QUAD'                   => "\xe2\x80\x80",
108
        'EM QUAD'                   => "\xe2\x80\x81",
109
        'EN SPACE'                  => "\xe2\x80\x82",
110
        'EM SPACE'                  => "\xe2\x80\x83",
111
        'THREE-PER-EM SPACE'        => "\xe2\x80\x84",
112
        'FOUR-PER-EM SPACE'         => "\xe2\x80\x85",
113
        'SIX-PER-EM SPACE'          => "\xe2\x80\x86",
114
        'FIGURE SPACE'              => "\xe2\x80\x87",
115
        'PUNCTUATION SPACE'         => "\xe2\x80\x88",
116
        'THIN SPACE'                => "\xe2\x80\x89",
117
        'HAIR SPACE'                => "\xe2\x80\x8a",
118
        'LINE SEPARATOR'            => "\xe2\x80\xa8",
119
        'PARAGRAPH SEPARATOR'       => "\xe2\x80\xa9",
120
        'ZERO WIDTH SPACE'          => "\xe2\x80\x8b",
121
        'NARROW NO-BREAK SPACE'     => "\xe2\x80\xaf",
122
        'MEDIUM MATHEMATICAL SPACE' => "\xe2\x81\x9f",
123
        'IDEOGRAPHIC SPACE'         => "\xe3\x80\x80",
124
        'HALFWIDTH HANGUL FILLER'   => "\xef\xbe\xa0",
125
    ];
126
127
    /**
128
     * @var array{upper: string[], lower: string[]}
129
     */
130
    private static $COMMON_CASE_FOLD = [
131
        'upper' => [
132
            'µ',
133
            'ſ',
134
            "\xCD\x85",
135
            'ς',
136
            'ẞ',
137
            "\xCF\x90",
138
            "\xCF\x91",
139
            "\xCF\x95",
140
            "\xCF\x96",
141
            "\xCF\xB0",
142
            "\xCF\xB1",
143
            "\xCF\xB5",
144
            "\xE1\xBA\x9B",
145
            "\xE1\xBE\xBE",
146
        ],
147
        'lower' => [
148
            'μ',
149
            's',
150
            'ι',
151
            'σ',
152
            'ß',
153
            'β',
154
            'θ',
155
            'φ',
156
            'π',
157
            'κ',
158
            'ρ',
159
            'ε',
160
            "\xE1\xB9\xA1",
161
            'ι',
162
        ],
163
    ];
164
165
    /**
166
     * @var array<string, mixed>
167
     */
168
    private static $SUPPORT = [];
169
170
    /**
171
     * @var array<string, string>|null
172
     */
173
    private static $BROKEN_UTF8_FIX;
174
175
    /**
176
     * @var array<int, string>|null
177
     */
178
    private static $WIN1252_TO_UTF8;
179
180
    /**
181
     * @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...
182
     */
183
    private static $INTL_TRANSLITERATOR_LIST;
184
185
    /**
186
     * @var array<string>|null
187
     */
188
    private static $ENCODINGS;
189
190
    /**
191
     * @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...
192
     */
193
    private static $ORD;
194
195
    /**
196
     * @var array<string, string>|null
197
     */
198
    private static $EMOJI;
199
200
    /**
201
     * @var array<string>|null
202
     */
203
    private static $EMOJI_VALUES_CACHE;
204
205
    /**
206
     * @var array<string>|null
207
     */
208
    private static $EMOJI_KEYS_CACHE;
209
210
    /**
211
     * @var array<string>|null
212
     */
213
    private static $EMOJI_KEYS_REVERSIBLE_CACHE;
214
215
    /**
216
     * @var array<int, string>|null
217
     */
218
    private static $CHR;
219
220
    /**
221
     * __construct()
222
     */
223 34
    public function __construct()
224
    {
225 34
    }
226
227
    /**
228
     * Return the character at the specified position: $str[1] like functionality.
229
     *
230
     * @param string $str      <p>A UTF-8 string.</p>
231
     * @param int    $pos      <p>The position of character to return.</p>
232
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
233
     *
234
     * @psalm-pure
235
     *
236
     * @return string single multi-byte character
237
     */
238 3
    public static function access(string $str, int $pos, string $encoding = 'UTF-8'): string
239
    {
240 3
        if ($str === '' || $pos < 0) {
241 2
            return '';
242
        }
243
244 3
        if ($encoding === 'UTF-8') {
245 3
            return (string) \mb_substr($str, $pos, 1);
246
        }
247
248
        return (string) self::substr($str, $pos, 1, $encoding);
249
    }
250
251
    /**
252
     * Prepends UTF-8 BOM character to the string and returns the whole string.
253
     *
254
     * INFO: If BOM already existed there, the Input string is returned.
255
     *
256
     * @param string $str <p>The input string.</p>
257
     *
258
     * @psalm-pure
259
     *
260
     * @return string the output string that contains BOM
261
     */
262 2
    public static function add_bom_to_string(string $str): string
263
    {
264 2
        if (self::string_has_bom($str) === false) {
265 2
            $str = self::bom() . $str;
266
        }
267
268 2
        return $str;
269
    }
270
271
    /**
272
     * Changes all keys in an array.
273
     *
274
     * @param array<string, mixed> $array    <p>The array to work on</p>
275
     * @param int                  $case     [optional] <p> Either <strong>CASE_UPPER</strong><br>
276
     *                                       or <strong>CASE_LOWER</strong> (default)</p>
277
     * @param string               $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
278
     *
279
     * @psalm-pure
280
     *
281
     * @return string[]
282
     *                  <p>An array with its keys lower- or uppercased.</p>
283
     */
284 2
    public static function array_change_key_case(
285
        array $array,
286
        int $case = \CASE_LOWER,
287
        string $encoding = 'UTF-8'
288
    ): array {
289
        if (
290 2
            $case !== \CASE_LOWER
291
            &&
292 2
            $case !== \CASE_UPPER
293
        ) {
294
            $case = \CASE_LOWER;
295
        }
296
297 2
        $return = [];
298 2
        foreach ($array as $key => &$value) {
299 2
            $key = $case === \CASE_LOWER
300 2
                ? self::strtolower((string) $key, $encoding)
301 2
                : self::strtoupper((string) $key, $encoding);
302
303 2
            $return[$key] = $value;
304
        }
305
306 2
        return $return;
307
    }
308
309
    /**
310
     * Returns the substring between $start and $end, if found, or an empty
311
     * string. An optional offset may be supplied from which to begin the
312
     * search for the start string.
313
     *
314
     * @param string $str
315
     * @param string $start    <p>Delimiter marking the start of the substring.</p>
316
     * @param string $end      <p>Delimiter marking the end of the substring.</p>
317
     * @param int    $offset   [optional] <p>Index from which to begin the search. Default: 0</p>
318
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
319
     *
320
     * @psalm-pure
321
     *
322
     * @return string
323
     */
324 16
    public static function between(
325
        string $str,
326
        string $start,
327
        string $end,
328
        int $offset = 0,
329
        string $encoding = 'UTF-8'
330
    ): string {
331 16
        if ($encoding === 'UTF-8') {
332 8
            $start_position = \mb_strpos($str, $start, $offset);
333 8
            if ($start_position === false) {
334 1
                return '';
335
            }
336
337 7
            $substr_index = $start_position + (int) \mb_strlen($start);
338 7
            $end_position = \mb_strpos($str, $end, $substr_index);
339
            if (
340 7
                $end_position === false
341
                ||
342 7
                $end_position === $substr_index
343
            ) {
344 2
                return '';
345
            }
346
347 5
            return (string) \mb_substr($str, $substr_index, $end_position - $substr_index);
348
        }
349
350 8
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
351
352 8
        $start_position = self::strpos($str, $start, $offset, $encoding);
353 8
        if ($start_position === false) {
354 1
            return '';
355
        }
356
357 7
        $substr_index = $start_position + (int) self::strlen($start, $encoding);
358 7
        $end_position = self::strpos($str, $end, $substr_index, $encoding);
359
        if (
360 7
            $end_position === false
361
            ||
362 7
            $end_position === $substr_index
363
        ) {
364 2
            return '';
365
        }
366
367 5
        return (string) self::substr(
368 5
            $str,
369 5
            $substr_index,
370 5
            $end_position - $substr_index,
371 5
            $encoding
372
        );
373
    }
374
375
    /**
376
     * Convert binary into a string.
377
     *
378
     * @param mixed $bin 1|0
379
     *
380
     * @psalm-pure
381
     *
382
     * @return string
383
     */
384 2
    public static function binary_to_str($bin): string
385
    {
386 2
        if (!isset($bin[0])) {
387
            return '';
388
        }
389
390 2
        $convert = \base_convert($bin, 2, 16);
391 2
        if ($convert === '0') {
392 1
            return '';
393
        }
394
395 2
        return \pack('H*', $convert);
396
    }
397
398
    /**
399
     * Returns the UTF-8 Byte Order Mark Character.
400
     *
401
     * INFO: take a look at UTF8::$bom for e.g. UTF-16 and UTF-32 BOM values
402
     *
403
     * @psalm-pure
404
     *
405
     * @return string UTF-8 Byte Order Mark
406
     */
407 4
    public static function bom(): string
408
    {
409 4
        return "\xef\xbb\xbf";
410
    }
411
412
    /**
413
     * @alias of UTF8::chr_map()
414
     *
415
     * @param callable $callback
416
     * @param string   $str
417
     *
418
     * @psalm-pure
419
     *
420
     * @return string[]
421
     *
422
     * @see   UTF8::chr_map()
423
     */
424 2
    public static function callback($callback, string $str): array
425
    {
426 2
        return self::chr_map($callback, $str);
427
    }
428
429
    /**
430
     * Returns the character at $index, with indexes starting at 0.
431
     *
432
     * @param string $str      <p>The input string.</p>
433
     * @param int    $index    <p>Position of the character.</p>
434
     * @param string $encoding [optional] <p>Default is UTF-8</p>
435
     *
436
     * @psalm-pure
437
     *
438
     * @return string the character at $index
439
     */
440 9
    public static function char_at(string $str, int $index, string $encoding = 'UTF-8'): string
441
    {
442 9
        if ($encoding === 'UTF-8') {
443 5
            return (string) \mb_substr($str, $index, 1);
444
        }
445
446 4
        return (string) self::substr($str, $index, 1, $encoding);
447
    }
448
449
    /**
450
     * Returns an array consisting of the characters in the string.
451
     *
452
     * @param string $str <p>The input string.</p>
453
     *
454
     * @psalm-pure
455
     *
456
     * @return string[] an array of chars
457
     */
458 3
    public static function chars(string $str): array
459
    {
460 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 which are incompatible with the documented value type string.
Loading history...
461
    }
462
463
    /**
464
     * This method will auto-detect your server environment for UTF-8 support.
465
     *
466
     * @return true|null
467
     *
468
     * @internal <p>You don't need to run it manually, it will be triggered if it's needed.</p>
469
     */
470 5
    public static function checkForSupport()
471
    {
472 5
        if (!isset(self::$SUPPORT['already_checked_via_portable_utf8'])) {
473
            self::$SUPPORT['already_checked_via_portable_utf8'] = true;
474
475
            // http://php.net/manual/en/book.mbstring.php
476
            self::$SUPPORT['mbstring'] = self::mbstring_loaded();
477
            self::$SUPPORT['mbstring_func_overload'] = self::mbstring_overloaded();
478
            if (self::$SUPPORT['mbstring'] === true) {
479
                \mb_internal_encoding('UTF-8');
480
                /** @noinspection UnusedFunctionResultInspection */
481
                /** @noinspection PhpComposerExtensionStubsInspection */
482
                \mb_regex_encoding('UTF-8');
483
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
484
            }
485
486
            // http://php.net/manual/en/book.iconv.php
487
            self::$SUPPORT['iconv'] = self::iconv_loaded();
488
489
            // http://php.net/manual/en/book.intl.php
490
            self::$SUPPORT['intl'] = self::intl_loaded();
491
492
            // http://php.net/manual/en/class.intlchar.php
493
            self::$SUPPORT['intlChar'] = self::intlChar_loaded();
494
495
            // http://php.net/manual/en/book.ctype.php
496
            self::$SUPPORT['ctype'] = self::ctype_loaded();
497
498
            // http://php.net/manual/en/class.finfo.php
499
            self::$SUPPORT['finfo'] = self::finfo_loaded();
500
501
            // http://php.net/manual/en/book.json.php
502
            self::$SUPPORT['json'] = self::json_loaded();
503
504
            // http://php.net/manual/en/book.pcre.php
505
            self::$SUPPORT['pcre_utf8'] = self::pcre_utf8_support();
506
507
            self::$SUPPORT['symfony_polyfill_used'] = self::symfony_polyfill_used();
508
            if (self::$SUPPORT['symfony_polyfill_used'] === true) {
509
                \mb_internal_encoding('UTF-8');
510
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
511
            }
512
513
            return true;
514
        }
515
516 5
        return null;
517
    }
518
519
    /**
520
     * Generates a UTF-8 encoded character from the given code point.
521
     *
522
     * INFO: opposite to UTF8::ord()
523
     *
524
     * @param int|string $code_point <p>The code point for which to generate a character.</p>
525
     * @param string     $encoding   [optional] <p>Default is UTF-8</p>
526
     *
527
     * @psalm-pure
528
     *
529
     * @return string|null multi-byte character, returns null on failure or empty input
530
     */
531 21
    public static function chr($code_point, string $encoding = 'UTF-8')
532
    {
533
        // init
534
        /**
535
         * @psalm-suppress ImpureStaticVariable
536
         *
537
         * @var array<string,string>
538
         */
539 21
        static $CHAR_CACHE = [];
540
541 21
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
542 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
543
        }
544
545
        if (
546 21
            $encoding !== 'UTF-8'
547
            &&
548 21
            $encoding !== 'ISO-8859-1'
549
            &&
550 21
            $encoding !== 'WINDOWS-1252'
551
            &&
552 21
            self::$SUPPORT['mbstring'] === false
553
        ) {
554
            /**
555
             * @psalm-suppress ImpureFunctionCall - is is only a warning
556
             */
557
            \trigger_error('UTF8::chr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
558
        }
559
560 21
        $cache_key = $code_point . '_' . $encoding;
561 21
        if (isset($CHAR_CACHE[$cache_key]) === true) {
562 19
            return $CHAR_CACHE[$cache_key];
563
        }
564
565 12
        if ($code_point <= 127) { // use "simple"-char only until "\x80"
566
567 12
            if (self::$CHR === null) {
568
                self::$CHR = self::getData('chr');
569
            }
570
571
            /**
572
             * @psalm-suppress PossiblyNullArrayAccess
573
             */
574 12
            $chr = self::$CHR[$code_point];
575
576 12
            if ($encoding !== 'UTF-8') {
577 1
                $chr = self::encode($encoding, $chr);
578
            }
579
580 12
            return $CHAR_CACHE[$cache_key] = $chr;
581
        }
582
583
        //
584
        // fallback via "IntlChar"
585
        //
586
587 5
        if (self::$SUPPORT['intlChar'] === true) {
588
            /** @noinspection PhpComposerExtensionStubsInspection */
589 5
            $chr = \IntlChar::chr($code_point);
590
591 5
            if ($encoding !== 'UTF-8') {
592
                $chr = self::encode($encoding, $chr);
593
            }
594
595 5
            return $CHAR_CACHE[$cache_key] = $chr;
596
        }
597
598
        //
599
        // fallback via vanilla php
600
        //
601
602
        if (self::$CHR === null) {
603
            self::$CHR = self::getData('chr');
604
        }
605
606
        $code_point = (int) $code_point;
607
        if ($code_point <= 0x7F) {
608
            /**
609
             * @psalm-suppress PossiblyNullArrayAccess
610
             */
611
            $chr = self::$CHR[$code_point];
612
        } elseif ($code_point <= 0x7FF) {
613
            /**
614
             * @psalm-suppress PossiblyNullArrayAccess
615
             */
616
            $chr = self::$CHR[($code_point >> 6) + 0xC0] .
617
                   self::$CHR[($code_point & 0x3F) + 0x80];
618
        } elseif ($code_point <= 0xFFFF) {
619
            /**
620
             * @psalm-suppress PossiblyNullArrayAccess
621
             */
622
            $chr = self::$CHR[($code_point >> 12) + 0xE0] .
623
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
624
                   self::$CHR[($code_point & 0x3F) + 0x80];
625
        } else {
626
            /**
627
             * @psalm-suppress PossiblyNullArrayAccess
628
             */
629
            $chr = self::$CHR[($code_point >> 18) + 0xF0] .
630
                   self::$CHR[(($code_point >> 12) & 0x3F) + 0x80] .
631
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
632
                   self::$CHR[($code_point & 0x3F) + 0x80];
633
        }
634
635
        if ($encoding !== 'UTF-8') {
636
            $chr = self::encode($encoding, $chr);
637
        }
638
639
        return $CHAR_CACHE[$cache_key] = $chr;
640
    }
641
642
    /**
643
     * Applies callback to all characters of a string.
644
     *
645
     * @param callable $callback <p>The callback function.</p>
646
     * @param string   $str      <p>UTF-8 string to run callback on.</p>
647
     *
648
     * @psalm-pure
649
     *
650
     * @return string[]
651
     *                  <p>The outcome of the callback, as array.</p>
652
     */
653 2
    public static function chr_map($callback, string $str): array
654
    {
655 2
        return \array_map(
656 2
            $callback,
657 2
            self::str_split($str)
658
        );
659
    }
660
661
    /**
662
     * Generates an array of byte length of each character of a Unicode string.
663
     *
664
     * 1 byte => U+0000  - U+007F
665
     * 2 byte => U+0080  - U+07FF
666
     * 3 byte => U+0800  - U+FFFF
667
     * 4 byte => U+10000 - U+10FFFF
668
     *
669
     * @param string $str <p>The original unicode string.</p>
670
     *
671
     * @psalm-pure
672
     *
673
     * @return int[] an array of byte lengths of each character
674
     */
675 4
    public static function chr_size_list(string $str): array
676
    {
677 4
        if ($str === '') {
678 4
            return [];
679
        }
680
681 4
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
682
            return \array_map(
683
                static function (string $data): int {
684
                    // "mb_" is available if overload is used, so use it ...
685
                    return \mb_strlen($data, 'CP850'); // 8-BIT
686
                },
687
                self::str_split($str)
688
            );
689
        }
690
691 4
        return \array_map('\strlen', self::str_split($str));
692
    }
693
694
    /**
695
     * Get a decimal code representation of a specific character.
696
     *
697
     * @param string $char <p>The input character.</p>
698
     *
699
     * @psalm-pure
700
     *
701
     * @return int
702
     */
703 4
    public static function chr_to_decimal(string $char): int
704
    {
705 4
        if (self::$SUPPORT['iconv'] === true) {
706 4
            $chr_tmp = \iconv('UTF-8', 'UCS-4LE', $char);
707 4
            if ($chr_tmp !== false) {
708
                /** @noinspection OffsetOperationsInspection */
709 4
                return \unpack('V', $chr_tmp)[1];
710
            }
711
        }
712
713
        $code = self::ord($char[0]);
714
        $bytes = 1;
715
716
        if (!($code & 0x80)) {
717
            // 0xxxxxxx
718
            return $code;
719
        }
720
721
        if (($code & 0xe0) === 0xc0) {
722
            // 110xxxxx
723
            $bytes = 2;
724
            $code &= ~0xc0;
725
        } elseif (($code & 0xf0) === 0xe0) {
726
            // 1110xxxx
727
            $bytes = 3;
728
            $code &= ~0xe0;
729
        } elseif (($code & 0xf8) === 0xf0) {
730
            // 11110xxx
731
            $bytes = 4;
732
            $code &= ~0xf0;
733
        }
734
735
        for ($i = 2; $i <= $bytes; ++$i) {
736
            // 10xxxxxx
737
            $code = ($code << 6) + (self::ord($char[$i - 1]) & ~0x80);
738
        }
739
740
        return $code;
741
    }
742
743
    /**
744
     * Get hexadecimal code point (U+xxxx) of a UTF-8 encoded character.
745
     *
746
     * @param int|string $char   <p>The input character</p>
747
     * @param string     $prefix [optional]
748
     *
749
     * @psalm-pure
750
     *
751
     * @return string The code point encoded as U+xxxx
752
     */
753 2
    public static function chr_to_hex($char, string $prefix = 'U+'): string
754
    {
755 2
        if ($char === '') {
756 2
            return '';
757
        }
758
759 2
        if ($char === '&#0;') {
760 2
            $char = '';
761
        }
762
763 2
        return self::int_to_hex(self::ord((string) $char), $prefix);
764
    }
765
766
    /**
767
     * alias for "UTF8::chr_to_decimal()"
768
     *
769
     * @param string $chr
770
     *
771
     * @psalm-pure
772
     *
773
     * @return int
774
     *
775
     * @see        UTF8::chr_to_decimal()
776
     * @deprecated <p>please use "UTF8::chr_to_decimal()"</p>
777
     */
778 2
    public static function chr_to_int(string $chr): int
779
    {
780 2
        return self::chr_to_decimal($chr);
781
    }
782
783
    /**
784
     * Splits a string into smaller chunks and multiple lines, using the specified line ending character.
785
     *
786
     * @param string $body         <p>The original string to be split.</p>
787
     * @param int    $chunk_length [optional] <p>The maximum character length of a chunk.</p>
788
     * @param string $end          [optional] <p>The character(s) to be inserted at the end of each chunk.</p>
789
     *
790
     * @psalm-pure
791
     *
792
     * @return string the chunked string
793
     */
794 4
    public static function chunk_split(string $body, int $chunk_length = 76, string $end = "\r\n"): string
795
    {
796 4
        return \implode($end, self::str_split($body, $chunk_length));
797
    }
798
799
    /**
800
     * Accepts a string and removes all non-UTF-8 characters from it + extras if needed.
801
     *
802
     * @param string $str                                     <p>The string to be sanitized.</p>
803
     * @param bool   $remove_bom                              [optional] <p>Set to true, if you need to remove
804
     *                                                        UTF-BOM.</p>
805
     * @param bool   $normalize_whitespace                    [optional] <p>Set to true, if you need to normalize the
806
     *                                                        whitespace.</p>
807
     * @param bool   $normalize_msword                        [optional] <p>Set to true, if you need to normalize MS
808
     *                                                        Word chars e.g.: "…"
809
     *                                                        => "..."</p>
810
     * @param bool   $keep_non_breaking_space                 [optional] <p>Set to true, to keep non-breaking-spaces,
811
     *                                                        in
812
     *                                                        combination with
813
     *                                                        $normalize_whitespace</p>
814
     * @param bool   $replace_diamond_question_mark           [optional] <p>Set to true, if you need to remove diamond
815
     *                                                        question mark e.g.: "�"</p>
816
     * @param bool   $remove_invisible_characters             [optional] <p>Set to false, if you not want to remove
817
     *                                                        invisible characters e.g.: "\0"</p>
818
     * @param bool   $remove_invisible_characters_url_encoded [optional] <p>Set to true, if you not want to remove
819
     *                                                        invisible url encoded characters e.g.: "%0B"<br> WARNING:
820
     *                                                        maybe contains false-positives e.g. aa%0Baa -> aaaa.
821
     *                                                        </p>
822
     *
823
     * @psalm-pure
824
     *
825
     * @return string clean UTF-8 encoded string
826
     */
827 87
    public static function clean(
828
        string $str,
829
        bool $remove_bom = false,
830
        bool $normalize_whitespace = false,
831
        bool $normalize_msword = false,
832
        bool $keep_non_breaking_space = false,
833
        bool $replace_diamond_question_mark = false,
834
        bool $remove_invisible_characters = true,
835
        bool $remove_invisible_characters_url_encoded = false
836
    ): string {
837
        // http://stackoverflow.com/questions/1401317/remove-non-utf8-characters-from-string
838
        // caused connection reset problem on larger strings
839
840 87
        $regex = '/
841
          (
842
            (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
843
            |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
844
            |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
845
            |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3
846
            ){1,100}                      # ...one or more times
847
          )
848
        | ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
849
        | ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
850
        /x';
851
        /** @noinspection NotOptimalRegularExpressionsInspection */
852 87
        $str = (string) \preg_replace($regex, '$1', $str);
853
854 87
        if ($replace_diamond_question_mark === true) {
855 33
            $str = self::replace_diamond_question_mark($str, '');
856
        }
857
858 87
        if ($remove_invisible_characters === true) {
859 87
            $str = self::remove_invisible_characters($str, $remove_invisible_characters_url_encoded);
860
        }
861
862 87
        if ($normalize_whitespace === true) {
863 37
            $str = self::normalize_whitespace($str, $keep_non_breaking_space);
864
        }
865
866 87
        if ($normalize_msword === true) {
867 4
            $str = self::normalize_msword($str);
868
        }
869
870 87
        if ($remove_bom === true) {
871 37
            $str = self::remove_bom($str);
872
        }
873
874 87
        return $str;
875
    }
876
877
    /**
878
     * Clean-up a string and show only printable UTF-8 chars at the end  + fix UTF-8 encoding.
879
     *
880
     * @param string $str <p>The input string.</p>
881
     *
882
     * @psalm-pure
883
     *
884
     * @return string
885
     */
886 33
    public static function cleanup($str): string
887
    {
888
        // init
889 33
        $str = (string) $str;
890
891 33
        if ($str === '') {
892 5
            return '';
893
        }
894
895
        // fixed ISO <-> UTF-8 Errors
896 33
        $str = self::fix_simple_utf8($str);
897
898
        // remove all none UTF-8 symbols
899
        // && remove diamond question mark (�)
900
        // && remove remove invisible characters (e.g. "\0")
901
        // && remove BOM
902
        // && normalize whitespace chars (but keep non-breaking-spaces)
903 33
        return self::clean(
904 33
            $str,
905 33
            true,
906 33
            true,
907 33
            false,
908 33
            true,
909 33
            true,
910 33
            true
911
        );
912
    }
913
914
    /**
915
     * Accepts a string or a array of strings and returns an array of Unicode code points.
916
     *
917
     * INFO: opposite to UTF8::string()
918
     *
919
     * @param string|string[] $arg     <p>A UTF-8 encoded string or an array of such strings.</p>
920
     * @param bool            $u_style <p>If True, will return code points in U+xxxx format,
921
     *                                 default, code points will be returned as integers.</p>
922
     *
923
     * @psalm-pure
924
     *
925
     * @return array<int|string>
926
     *                           The array of code points:<br>
927
     *                           array<int> for $u_style === false<br>
928
     *                           array<string> for $u_style === true<br>
929
     */
930 12
    public static function codepoints($arg, bool $u_style = false): array
931
    {
932 12
        if (\is_string($arg) === true) {
933 12
            $arg = self::str_split($arg);
934
        }
935
936
        /**
937
         * @psalm-suppress DocblockTypeContradiction
938
         */
939 12
        if (!\is_array($arg)) {
0 ignored issues
show
introduced by
The condition is_array($arg) is always true.
Loading history...
940 4
            return [];
941
        }
942
943 12
        if ($arg === []) {
944 7
            return [];
945
        }
946
947 11
        $arg = \array_map(
948
            [
949 11
                self::class,
950
                'ord',
951
            ],
952 11
            $arg
953
        );
954
955 11
        if ($u_style === true) {
956 2
            $arg = \array_map(
957
                [
958 2
                    self::class,
959
                    'int_to_hex',
960
                ],
961 2
                $arg
962
            );
963
        }
964
965 11
        return $arg;
966
    }
967
968
    /**
969
     * Trims the string and replaces consecutive whitespace characters with a
970
     * single space. This includes tabs and newline characters, as well as
971
     * multibyte whitespace such as the thin space and ideographic space.
972
     *
973
     * @param string $str <p>The input string.</p>
974
     *
975
     * @psalm-pure
976
     *
977
     * @return string string with a trimmed $str and condensed whitespace
978
     */
979 13
    public static function collapse_whitespace(string $str): string
980
    {
981 13
        if (self::$SUPPORT['mbstring'] === true) {
982
            /** @noinspection PhpComposerExtensionStubsInspection */
983 13
            return \trim((string) \mb_ereg_replace('[[:space:]]+', ' ', $str));
984
        }
985
986
        return \trim(self::regex_replace($str, '[[:space:]]+', ' '));
987
    }
988
989
    /**
990
     * Returns count of characters used in a string.
991
     *
992
     * @param string $str                     <p>The input string.</p>
993
     * @param bool   $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
994
     * @param bool   $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
995
     *
996
     * @psalm-pure
997
     *
998
     * @return int[] an associative array of Character as keys and
999
     *               their count as values
1000
     */
1001 19
    public static function count_chars(
1002
        string $str,
1003
        bool $clean_utf8 = false,
1004
        bool $try_to_use_mb_functions = true
1005
    ): array {
1006 19
        return \array_count_values(
1007 19
            self::str_split(
1008 19
                $str,
1009 19
                1,
1010 19
                $clean_utf8,
1011 19
                $try_to_use_mb_functions
1012
            )
1013
        );
1014
    }
1015
1016
    /**
1017
     * Remove css media-queries.
1018
     *
1019
     * @param string $str
1020
     *
1021
     * @psalm-pure
1022
     *
1023
     * @return string
1024
     */
1025 1
    public static function css_stripe_media_queries(string $str): string
1026
    {
1027 1
        return (string) \preg_replace(
1028 1
            '#@media\\s+(?:only\\s)?(?:[\\s{(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#isumU',
1029 1
            '',
1030 1
            $str
1031
        );
1032
    }
1033
1034
    /**
1035
     * Checks whether ctype is available on the server.
1036
     *
1037
     * @psalm-pure
1038
     *
1039
     * @return bool
1040
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
1041
     */
1042
    public static function ctype_loaded(): bool
1043
    {
1044
        return \extension_loaded('ctype');
1045
    }
1046
1047
    /**
1048
     * Converts an int value into a UTF-8 character.
1049
     *
1050
     * @param mixed $int
1051
     *
1052
     * @psalm-pure
1053
     *
1054
     * @return string
1055
     */
1056 20
    public static function decimal_to_chr($int): string
1057
    {
1058 20
        return self::html_entity_decode('&#' . $int . ';', \ENT_QUOTES | \ENT_HTML5);
1059
    }
1060
1061
    /**
1062
     * Decodes a MIME header field
1063
     *
1064
     * @param string $str
1065
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
1066
     *
1067
     * @psalm-pure
1068
     *
1069
     * @return false|string
1070
     *                      A decoded MIME field on success,
1071
     *                      or false if an error occurs during the decoding
1072
     */
1073
    public static function decode_mimeheader($str, string $encoding = 'UTF-8')
1074
    {
1075
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1076
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1077
        }
1078
1079
        if (self::$SUPPORT['iconv'] === true) {
1080
            return \iconv_mime_decode($str, \ICONV_MIME_DECODE_CONTINUE_ON_ERROR, $encoding);
1081
        }
1082
1083
        if ($encoding !== 'UTF-8') {
1084
            $str = self::encode($encoding, $str);
1085
        }
1086
1087
        return \mb_decode_mimeheader($str);
1088
    }
1089
1090
    /**
1091
     * Decodes a string which was encoded by "UTF8::emoji_encode()".
1092
     *
1093
     * @param string $str                            <p>The input string.</p>
1094
     * @param bool   $use_reversible_string_mappings [optional] <p>
1095
     *                                               When <b>TRUE</b>, we se a reversible string mapping
1096
     *                                               between "emoji_encode" and "emoji_decode".</p>
1097
     *
1098
     * @psalm-pure
1099
     *
1100
     * @return string
1101
     */
1102 9
    public static function emoji_decode(
1103
        string $str,
1104
        bool $use_reversible_string_mappings = false
1105
    ): string {
1106 9
        self::initEmojiData();
1107
1108 9
        if ($use_reversible_string_mappings === true) {
1109 9
            return (string) \str_replace(
1110 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1111 9
                (array) self::$EMOJI_VALUES_CACHE,
1112 9
                $str
1113
            );
1114
        }
1115
1116 1
        return (string) \str_replace(
1117 1
            (array) self::$EMOJI_KEYS_CACHE,
1118 1
            (array) self::$EMOJI_VALUES_CACHE,
1119 1
            $str
1120
        );
1121
    }
1122
1123
    /**
1124
     * Encode a string with emoji chars into a non-emoji string.
1125
     *
1126
     * @param string $str                            <p>The input string</p>
1127
     * @param bool   $use_reversible_string_mappings [optional] <p>
1128
     *                                               when <b>TRUE</b>, we se a reversible string mapping
1129
     *                                               between "emoji_encode" and "emoji_decode"</p>
1130
     *
1131
     * @psalm-pure
1132
     *
1133
     * @return string
1134
     */
1135 12
    public static function emoji_encode(
1136
        string $str,
1137
        bool $use_reversible_string_mappings = false
1138
    ): string {
1139 12
        self::initEmojiData();
1140
1141 12
        if ($use_reversible_string_mappings === true) {
1142 9
            return (string) \str_replace(
1143 9
                (array) self::$EMOJI_VALUES_CACHE,
1144 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1145 9
                $str
1146
            );
1147
        }
1148
1149 4
        return (string) \str_replace(
1150 4
            (array) self::$EMOJI_VALUES_CACHE,
1151 4
            (array) self::$EMOJI_KEYS_CACHE,
1152 4
            $str
1153
        );
1154
    }
1155
1156
    /**
1157
     * Encode a string with a new charset-encoding.
1158
     *
1159
     * INFO:  This function will also try to fix broken / double encoding,
1160
     *        so you can call this function also on a UTF-8 string and you don't mess up the string.
1161
     *
1162
     * @param string $to_encoding                   <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.</p>
1163
     * @param string $str                           <p>The input string</p>
1164
     * @param bool   $auto_detect_the_from_encoding [optional] <p>Force the new encoding (we try to fix broken / double
1165
     *                                              encoding for UTF-8)<br> otherwise we auto-detect the current
1166
     *                                              string-encoding</p>
1167
     * @param string $from_encoding                 [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1168
     *                                              A empty string will trigger the autodetect anyway.</p>
1169
     *
1170
     * @psalm-pure
1171
     *
1172
     * @return string
1173
     *
1174
     * @psalm-suppress InvalidReturnStatement
1175
     */
1176 28
    public static function encode(
1177
        string $to_encoding,
1178
        string $str,
1179
        bool $auto_detect_the_from_encoding = true,
1180
        string $from_encoding = ''
1181
    ): string {
1182 28
        if ($str === '' || $to_encoding === '') {
1183 13
            return $str;
1184
        }
1185
1186 28
        if ($to_encoding !== 'UTF-8' && $to_encoding !== 'CP850') {
1187 7
            $to_encoding = self::normalize_encoding($to_encoding, 'UTF-8');
1188
        }
1189
1190 28
        if ($from_encoding && $from_encoding !== 'UTF-8' && $from_encoding !== 'CP850') {
1191 2
            $from_encoding = self::normalize_encoding($from_encoding, null);
1192
        }
1193
1194
        if (
1195 28
            $to_encoding
1196
            &&
1197 28
            $from_encoding
1198
            &&
1199 28
            $from_encoding === $to_encoding
1200
        ) {
1201
            return $str;
1202
        }
1203
1204 28
        if ($to_encoding === 'JSON') {
1205 1
            $return = self::json_encode($str);
1206 1
            if ($return === false) {
1207
                throw new \InvalidArgumentException('The input string [' . $str . '] can not be used for json_encode().');
1208
            }
1209
1210 1
            return $return;
1211
        }
1212 28
        if ($from_encoding === 'JSON') {
1213 1
            $str = self::json_decode($str);
1214 1
            $from_encoding = '';
1215
        }
1216
1217 28
        if ($to_encoding === 'BASE64') {
1218 2
            return \base64_encode($str);
1219
        }
1220 28
        if ($from_encoding === 'BASE64') {
1221 2
            $str = \base64_decode($str, true);
1222 2
            $from_encoding = '';
1223
        }
1224
1225 28
        if ($to_encoding === 'HTML-ENTITIES') {
1226 2
            return self::html_encode($str, true, 'UTF-8');
1227
        }
1228 28
        if ($from_encoding === 'HTML-ENTITIES') {
1229 2
            $str = self::html_entity_decode($str, \ENT_COMPAT, 'UTF-8');
1230 2
            $from_encoding = '';
1231
        }
1232
1233 28
        $from_encoding_auto_detected = false;
1234
        if (
1235 28
            $auto_detect_the_from_encoding === true
1236
            ||
1237 28
            !$from_encoding
1238
        ) {
1239 28
            $from_encoding_auto_detected = self::str_detect_encoding($str);
1240
        }
1241
1242
        // DEBUG
1243
        //var_dump($to_encoding, $from_encoding, $from_encoding_auto_detected, $str, "\n\n");
1244
1245 28
        if ($from_encoding_auto_detected !== false) {
1246
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1247 24
            $from_encoding = $from_encoding_auto_detected;
1248 7
        } elseif ($auto_detect_the_from_encoding === true) {
1249
            // fallback for the "autodetect"-mode
1250 7
            return self::to_utf8($str);
1251
        }
1252
1253
        if (
1254 24
            !$from_encoding
1255
            ||
1256 24
            $from_encoding === $to_encoding
1257
        ) {
1258 15
            return $str;
1259
        }
1260
1261
        if (
1262 19
            $to_encoding === 'UTF-8'
1263
            &&
1264
            (
1265 17
                $from_encoding === 'WINDOWS-1252'
1266
                ||
1267 19
                $from_encoding === 'ISO-8859-1'
1268
            )
1269
        ) {
1270 13
            return self::to_utf8($str);
1271
        }
1272
1273
        if (
1274 12
            $to_encoding === 'ISO-8859-1'
1275
            &&
1276
            (
1277 6
                $from_encoding === 'WINDOWS-1252'
1278
                ||
1279 12
                $from_encoding === 'UTF-8'
1280
            )
1281
        ) {
1282 6
            return self::to_iso8859($str);
1283
        }
1284
1285
        if (
1286 10
            $to_encoding !== 'UTF-8'
1287
            &&
1288 10
            $to_encoding !== 'ISO-8859-1'
1289
            &&
1290 10
            $to_encoding !== 'WINDOWS-1252'
1291
            &&
1292 10
            self::$SUPPORT['mbstring'] === false
1293
        ) {
1294
            /**
1295
             * @psalm-suppress ImpureFunctionCall - is is only a warning
1296
             */
1297
            \trigger_error('UTF8::encode() without mbstring cannot handle "' . $to_encoding . '" encoding', \E_USER_WARNING);
1298
        }
1299
1300 10
        if (self::$SUPPORT['mbstring'] === true) {
1301
            // warning: do not use the symfony polyfill here
1302 10
            $str_encoded = \mb_convert_encoding(
1303 10
                $str,
1304 10
                $to_encoding,
1305 10
                $from_encoding
1306
            );
1307
1308 10
            if ($str_encoded) {
1309 10
                return $str_encoded;
1310
            }
1311
        }
1312
1313
        $return = \iconv($from_encoding, $to_encoding, $str);
1314
        if ($return !== false) {
1315
            return $return;
1316
        }
1317
1318
        return $str;
1319
    }
1320
1321
    /**
1322
     * @param string $str
1323
     * @param string $from_charset      [optional] <p>Set the input charset.</p>
1324
     * @param string $to_charset        [optional] <p>Set the output charset.</p>
1325
     * @param string $transfer_encoding [optional] <p>Set the transfer encoding.</p>
1326
     * @param string $linefeed          [optional] <p>Set the used linefeed.</p>
1327
     * @param int    $indent            [optional] <p>Set the max length indent.</p>
1328
     *
1329
     * @psalm-pure
1330
     *
1331
     * @return false|string
1332
     *                      <p>An encoded MIME field on success,
1333
     *                      or false if an error occurs during the encoding.</p>
1334
     */
1335
    public static function encode_mimeheader(
1336
        $str,
1337
        $from_charset = 'UTF-8',
1338
        $to_charset = 'UTF-8',
1339
        $transfer_encoding = 'Q',
1340
        $linefeed = '\\r\\n',
1341
        $indent = 76
1342
    ) {
1343
        if ($from_charset !== 'UTF-8' && $from_charset !== 'CP850') {
1344
            $from_charset = self::normalize_encoding($from_charset, 'UTF-8');
1345
        }
1346
1347
        if ($to_charset !== 'UTF-8' && $to_charset !== 'CP850') {
1348
            $to_charset = self::normalize_encoding($to_charset, 'UTF-8');
1349
        }
1350
1351
        return \iconv_mime_encode(
1352
            '',
1353
            $str,
1354
            [
1355
                'scheme'           => $transfer_encoding,
1356
                'line-length'      => $indent,
1357
                'input-charset'    => $from_charset,
1358
                'output-charset'   => $to_charset,
1359
                'line-break-chars' => $linefeed,
1360
            ]
1361
        );
1362
    }
1363
1364
    /**
1365
     * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
1366
     *
1367
     * @param string   $str                       <p>The input string.</p>
1368
     * @param string   $search                    <p>The searched string.</p>
1369
     * @param int|null $length                    [optional] <p>Default: null === text->length / 2</p>
1370
     * @param string   $replacer_for_skipped_text [optional] <p>Default: …</p>
1371
     * @param string   $encoding                  [optional] <p>Set the charset for e.g. "mb_" function</p>
1372
     *
1373
     * @psalm-pure
1374
     *
1375
     * @return string
1376
     */
1377 1
    public static function extract_text(
1378
        string $str,
1379
        string $search = '',
1380
        int $length = null,
1381
        string $replacer_for_skipped_text = '…',
1382
        string $encoding = 'UTF-8'
1383
    ): string {
1384 1
        if ($str === '') {
1385 1
            return '';
1386
        }
1387
1388 1
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1389
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1390
        }
1391
1392 1
        $trim_chars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1393
1394 1
        if ($length === null) {
1395 1
            $length = (int) \round((int) self::strlen($str, $encoding) / 2, 0);
1396
        }
1397
1398 1
        if ($search === '') {
1399 1
            if ($encoding === 'UTF-8') {
1400 1
                if ($length > 0) {
1401 1
                    $string_length = (int) \mb_strlen($str);
1402 1
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1403
                } else {
1404 1
                    $end = 0;
1405
                }
1406
1407 1
                $pos = (int) \min(
1408 1
                    \mb_strpos($str, ' ', $end),
1409 1
                    \mb_strpos($str, '.', $end)
1410
                );
1411
            } else {
1412
                if ($length > 0) {
1413
                    $string_length = (int) self::strlen($str, $encoding);
1414
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1415
                } else {
1416
                    $end = 0;
1417
                }
1418
1419
                $pos = (int) \min(
1420
                    self::strpos($str, ' ', $end, $encoding),
1421
                    self::strpos($str, '.', $end, $encoding)
1422
                );
1423
            }
1424
1425 1
            if ($pos) {
1426 1
                if ($encoding === 'UTF-8') {
1427 1
                    $str_sub = \mb_substr($str, 0, $pos);
1428
                } else {
1429
                    $str_sub = self::substr($str, 0, $pos, $encoding);
1430
                }
1431
1432 1
                if ($str_sub === false) {
1433
                    return '';
1434
                }
1435
1436 1
                return \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1437
            }
1438
1439
            return $str;
1440
        }
1441
1442 1
        if ($encoding === 'UTF-8') {
1443 1
            $word_position = (int) \mb_stripos($str, $search);
1444 1
            $half_side = (int) ($word_position - $length / 2 + (int) \mb_strlen($search) / 2);
1445
        } else {
1446
            $word_position = (int) self::stripos($str, $search, 0, $encoding);
1447
            $half_side = (int) ($word_position - $length / 2 + (int) self::strlen($search, $encoding) / 2);
1448
        }
1449
1450 1
        $pos_start = 0;
1451 1
        if ($half_side > 0) {
1452 1
            if ($encoding === 'UTF-8') {
1453 1
                $half_text = \mb_substr($str, 0, $half_side);
1454
            } else {
1455
                $half_text = self::substr($str, 0, $half_side, $encoding);
1456
            }
1457 1
            if ($half_text !== false) {
1458 1
                if ($encoding === 'UTF-8') {
1459 1
                    $pos_start = (int) \max(
1460 1
                        \mb_strrpos($half_text, ' '),
1461 1
                        \mb_strrpos($half_text, '.')
1462
                    );
1463
                } else {
1464
                    $pos_start = (int) \max(
1465
                        self::strrpos($half_text, ' ', 0, $encoding),
1466
                        self::strrpos($half_text, '.', 0, $encoding)
1467
                    );
1468
                }
1469
            }
1470
        }
1471
1472 1
        if ($word_position && $half_side > 0) {
1473 1
            $offset = $pos_start + $length - 1;
1474 1
            $real_length = (int) self::strlen($str, $encoding);
1475
1476 1
            if ($offset > $real_length) {
1477
                $offset = $real_length;
1478
            }
1479
1480 1
            if ($encoding === 'UTF-8') {
1481 1
                $pos_end = (int) \min(
1482 1
                    \mb_strpos($str, ' ', $offset),
1483 1
                    \mb_strpos($str, '.', $offset)
1484 1
                ) - $pos_start;
1485
            } else {
1486
                $pos_end = (int) \min(
1487
                    self::strpos($str, ' ', $offset, $encoding),
1488
                    self::strpos($str, '.', $offset, $encoding)
1489
                ) - $pos_start;
1490
            }
1491
1492 1
            if (!$pos_end || $pos_end <= 0) {
1493 1
                if ($encoding === 'UTF-8') {
1494 1
                    $str_sub = \mb_substr($str, $pos_start, (int) \mb_strlen($str));
1495
                } else {
1496
                    $str_sub = self::substr($str, $pos_start, (int) self::strlen($str, $encoding), $encoding);
1497
                }
1498 1
                if ($str_sub !== false) {
1499 1
                    $extract = $replacer_for_skipped_text . \ltrim($str_sub, $trim_chars);
1500
                } else {
1501 1
                    $extract = '';
1502
                }
1503
            } else {
1504 1
                if ($encoding === 'UTF-8') {
1505 1
                    $str_sub = \mb_substr($str, $pos_start, $pos_end);
1506
                } else {
1507
                    $str_sub = self::substr($str, $pos_start, $pos_end, $encoding);
1508
                }
1509 1
                if ($str_sub !== false) {
1510 1
                    $extract = $replacer_for_skipped_text . \trim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1511
                } else {
1512 1
                    $extract = '';
1513
                }
1514
            }
1515
        } else {
1516 1
            $offset = $length - 1;
1517 1
            $true_length = (int) self::strlen($str, $encoding);
1518
1519 1
            if ($offset > $true_length) {
1520
                $offset = $true_length;
1521
            }
1522
1523 1
            if ($encoding === 'UTF-8') {
1524 1
                $pos_end = (int) \min(
1525 1
                    \mb_strpos($str, ' ', $offset),
1526 1
                    \mb_strpos($str, '.', $offset)
1527
                );
1528
            } else {
1529
                $pos_end = (int) \min(
1530
                    self::strpos($str, ' ', $offset, $encoding),
1531
                    self::strpos($str, '.', $offset, $encoding)
1532
                );
1533
            }
1534
1535 1
            if ($pos_end) {
1536 1
                if ($encoding === 'UTF-8') {
1537 1
                    $str_sub = \mb_substr($str, 0, $pos_end);
1538
                } else {
1539
                    $str_sub = self::substr($str, 0, $pos_end, $encoding);
1540
                }
1541 1
                if ($str_sub !== false) {
1542 1
                    $extract = \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1543
                } else {
1544 1
                    $extract = '';
1545
                }
1546
            } else {
1547 1
                $extract = $str;
1548
            }
1549
        }
1550
1551 1
        return $extract;
1552
    }
1553
1554
    /**
1555
     * Reads entire file into a string.
1556
     *
1557
     * WARNING: Do not use UTF-8 Option ($convert_to_utf8) for binary files (e.g.: images) !!!
1558
     *
1559
     * @see http://php.net/manual/en/function.file-get-contents.php
1560
     *
1561
     * @param string        $filename         <p>
1562
     *                                        Name of the file to read.
1563
     *                                        </p>
1564
     * @param bool          $use_include_path [optional] <p>
1565
     *                                        Prior to PHP 5, this parameter is called
1566
     *                                        use_include_path and is a bool.
1567
     *                                        As of PHP 5 the FILE_USE_INCLUDE_PATH can be used
1568
     *                                        to trigger include path
1569
     *                                        search.
1570
     *                                        </p>
1571
     * @param resource|null $context          [optional] <p>
1572
     *                                        A valid context resource created with
1573
     *                                        stream_context_create. If you don't need to use a
1574
     *                                        custom context, you can skip this parameter by &null;.
1575
     *                                        </p>
1576
     * @param int|null      $offset           [optional] <p>
1577
     *                                        The offset where the reading starts.
1578
     *                                        </p>
1579
     * @param int|null      $max_length       [optional] <p>
1580
     *                                        Maximum length of data read. The default is to read until end
1581
     *                                        of file is reached.
1582
     *                                        </p>
1583
     * @param int           $timeout          <p>The time in seconds for the timeout.</p>
1584
     * @param bool          $convert_to_utf8  <strong>WARNING!!!</strong> <p>Maybe you can't use this option for
1585
     *                                        some files, because they used non default utf-8 chars. Binary files
1586
     *                                        like images or pdf will not be converted.</p>
1587
     * @param string        $from_encoding    [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1588
     *                                        A empty string will trigger the autodetect anyway.</p>
1589
     *
1590
     * @psalm-pure
1591
     *
1592
     * @return false|string
1593
     *                      <p>The function returns the read data as string or <b>false</b> on failure.</p>
1594
     */
1595 12
    public static function file_get_contents(
1596
        string $filename,
1597
        bool $use_include_path = false,
1598
        $context = null,
1599
        int $offset = null,
1600
        int $max_length = null,
1601
        int $timeout = 10,
1602
        bool $convert_to_utf8 = true,
1603
        string $from_encoding = ''
1604
    ) {
1605
        // init
1606 12
        $filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
1607
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1608 12
        if ($filename === false) {
1609
            return false;
1610
        }
1611
1612 12
        if ($timeout && $context === null) {
1613 9
            $context = \stream_context_create(
1614
                [
1615
                    'http' => [
1616 9
                        'timeout' => $timeout,
1617
                    ],
1618
                ]
1619
            );
1620
        }
1621
1622 12
        if ($offset === null) {
1623 12
            $offset = 0;
1624
        }
1625
1626 12
        if (\is_int($max_length) === true) {
1627 2
            $data = \file_get_contents($filename, $use_include_path, $context, $offset, $max_length);
1628
        } else {
1629 12
            $data = \file_get_contents($filename, $use_include_path, $context, $offset);
1630
        }
1631
1632
        // return false on error
1633 12
        if ($data === false) {
1634
            return false;
1635
        }
1636
1637 12
        if ($convert_to_utf8 === true) {
1638
            if (
1639 12
                self::is_binary($data, true) !== true
1640
                ||
1641 9
                self::is_utf16($data, false) !== false
1642
                ||
1643 12
                self::is_utf32($data, false) !== false
1644
            ) {
1645 9
                $data = self::encode('UTF-8', $data, false, $from_encoding);
1646 9
                $data = self::cleanup($data);
1647
            }
1648
        }
1649
1650 12
        return $data;
1651
    }
1652
1653
    /**
1654
     * Checks if a file starts with BOM (Byte Order Mark) character.
1655
     *
1656
     * @param string $file_path <p>Path to a valid file.</p>
1657
     *
1658
     * @throws \RuntimeException if file_get_contents() returned false
1659
     *
1660
     * @return bool
1661
     *              <p><strong>true</strong> if the file has BOM at the start, <strong>false</strong> otherwise</p>
1662
     *
1663
     * @psalm-pure
1664
     */
1665 2
    public static function file_has_bom(string $file_path): bool
1666
    {
1667 2
        $file_content = \file_get_contents($file_path);
1668 2
        if ($file_content === false) {
1669
            throw new \RuntimeException('file_get_contents() returned false for:' . $file_path);
1670
        }
1671
1672 2
        return self::string_has_bom($file_content);
1673
    }
1674
1675
    /**
1676
     * Normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1677
     *
1678
     * @param mixed  $var
1679
     * @param int    $normalization_form
1680
     * @param string $leading_combining
1681
     *
1682
     * @psalm-pure
1683
     *
1684
     * @return mixed
1685
     */
1686 62
    public static function filter(
1687
        $var,
1688
        int $normalization_form = \Normalizer::NFC,
1689
        string $leading_combining = '◌'
1690
    ) {
1691 62
        switch (\gettype($var)) {
1692 62
            case 'array':
1693
                /** @noinspection ForeachSourceInspection */
1694 6
                foreach ($var as $k => &$v) {
1695 6
                    $v = self::filter($v, $normalization_form, $leading_combining);
1696
                }
1697 6
                unset($v);
1698
1699 6
                break;
1700 62
            case 'object':
1701
                /** @noinspection ForeachSourceInspection */
1702 4
                foreach ($var as $k => &$v) {
1703 4
                    $v = self::filter($v, $normalization_form, $leading_combining);
1704
                }
1705 4
                unset($v);
1706
1707 4
                break;
1708 62
            case 'string':
1709
1710 62
                if (\strpos($var, "\r") !== false) {
1711
                    // Workaround https://bugs.php.net/65732
1712 3
                    $var = self::normalize_line_ending($var);
1713
                }
1714
1715 62
                if (ASCII::is_ascii($var) === false) {
1716 32
                    if (\Normalizer::isNormalized($var, $normalization_form)) {
1717 27
                        $n = '-';
1718
                    } else {
1719 12
                        $n = \Normalizer::normalize($var, $normalization_form);
1720
1721 12
                        if (isset($n[0])) {
1722 7
                            $var = $n;
1723
                        } else {
1724 8
                            $var = self::encode('UTF-8', $var, true);
1725
                        }
1726
                    }
1727
1728
                    if (
1729 32
                        $var[0] >= "\x80"
1730
                        &&
1731 32
                        isset($n[0], $leading_combining[0])
1732
                        &&
1733 32
                        \preg_match('/^\\p{Mn}/u', $var)
1734
                    ) {
1735
                        // Prevent leading combining chars
1736
                        // for NFC-safe concatenations.
1737 3
                        $var = $leading_combining . $var;
1738
                    }
1739
                }
1740
1741 62
                break;
1742
        }
1743
1744 62
        return $var;
1745
    }
1746
1747
    /**
1748
     * "filter_input()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1749
     *
1750
     * Gets a specific external variable by name and optionally filters it
1751
     *
1752
     * @see http://php.net/manual/en/function.filter-input.php
1753
     *
1754
     * @param int    $type          <p>
1755
     *                              One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1756
     *                              <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1757
     *                              <b>INPUT_ENV</b>.
1758
     *                              </p>
1759
     * @param string $variable_name <p>
1760
     *                              Name of a variable to get.
1761
     *                              </p>
1762
     * @param int    $filter        [optional] <p>
1763
     *                              The ID of the filter to apply. The
1764
     *                              manual page lists the available filters.
1765
     *                              </p>
1766
     * @param mixed  $options       [optional] <p>
1767
     *                              Associative array of options or bitwise disjunction of flags. If filter
1768
     *                              accepts options, flags can be provided in "flags" field of array.
1769
     *                              </p>
1770
     *
1771
     * @psalm-pure
1772
     *
1773
     * @return mixed Value of the requested variable on success, <b>FALSE</b> if the filter fails, or <b>NULL</b> if the
1774
     *               <i>variable_name</i> variable is not set. If the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it
1775
     *               returns <b>FALSE</b> if the variable is not set and <b>NULL</b> if the filter fails.
1776
     */
1777
    public static function filter_input(
1778
        int $type,
1779
        string $variable_name,
1780
        int $filter = \FILTER_DEFAULT,
1781
        $options = null
1782
    ) {
1783
        /**
1784
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1785
         */
1786
        if ($options === null || \func_num_args() < 4) {
1787
            $var = \filter_input($type, $variable_name, $filter);
1788
        } else {
1789
            $var = \filter_input($type, $variable_name, $filter, $options);
1790
        }
1791
1792
        return self::filter($var);
1793
    }
1794
1795
    /**
1796
     * "filter_input_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1797
     *
1798
     * Gets external variables and optionally filters them
1799
     *
1800
     * @see http://php.net/manual/en/function.filter-input-array.php
1801
     *
1802
     * @param int   $type       <p>
1803
     *                          One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1804
     *                          <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1805
     *                          <b>INPUT_ENV</b>.
1806
     *                          </p>
1807
     * @param mixed $definition [optional] <p>
1808
     *                          An array defining the arguments. A valid key is a string
1809
     *                          containing a variable name and a valid value is either a filter type, or an array
1810
     *                          optionally specifying the filter, flags and options. If the value is an
1811
     *                          array, valid keys are filter which specifies the
1812
     *                          filter type,
1813
     *                          flags which specifies any flags that apply to the
1814
     *                          filter, and options which specifies any options that
1815
     *                          apply to the filter. See the example below for a better understanding.
1816
     *                          </p>
1817
     *                          <p>
1818
     *                          This parameter can be also an integer holding a filter constant. Then all values in the
1819
     *                          input array are filtered by this filter.
1820
     *                          </p>
1821
     * @param bool  $add_empty  [optional] <p>
1822
     *                          Add missing keys as <b>NULL</b> to the return value.
1823
     *                          </p>
1824
     *
1825
     * @psalm-pure
1826
     *
1827
     * @return mixed An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1828
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1829
     *               set. Or if the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it returns <b>FALSE</b> if the variable
1830
     *               is not set and <b>NULL</b> if the filter fails.
1831
     */
1832
    public static function filter_input_array(
1833
        int $type,
1834
        $definition = null,
1835
        bool $add_empty = true
1836
    ) {
1837
        /**
1838
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1839
         */
1840
        if ($definition === null || \func_num_args() < 2) {
1841
            $a = \filter_input_array($type);
1842
        } else {
1843
            $a = \filter_input_array($type, $definition, $add_empty);
1844
        }
1845
1846
        return self::filter($a);
1847
    }
1848
1849
    /**
1850
     * "filter_var()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1851
     *
1852
     * Filters a variable with a specified filter
1853
     *
1854
     * @see http://php.net/manual/en/function.filter-var.php
1855
     *
1856
     * @param mixed $variable <p>
1857
     *                        Value to filter.
1858
     *                        </p>
1859
     * @param int   $filter   [optional] <p>
1860
     *                        The ID of the filter to apply. The
1861
     *                        manual page lists the available filters.
1862
     *                        </p>
1863
     * @param mixed $options  [optional] <p>
1864
     *                        Associative array of options or bitwise disjunction of flags. If filter
1865
     *                        accepts options, flags can be provided in "flags" field of array. For
1866
     *                        the "callback" filter, callable type should be passed. The
1867
     *                        callback must accept one argument, the value to be filtered, and return
1868
     *                        the value after filtering/sanitizing it.
1869
     *                        </p>
1870
     *                        <p>
1871
     *                        <code>
1872
     *                        // for filters that accept options, use this format
1873
     *                        $options = array(
1874
     *                        'options' => array(
1875
     *                        'default' => 3, // value to return if the filter fails
1876
     *                        // other options here
1877
     *                        'min_range' => 0
1878
     *                        ),
1879
     *                        'flags' => FILTER_FLAG_ALLOW_OCTAL,
1880
     *                        );
1881
     *                        $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
1882
     *                        // for filter that only accept flags, you can pass them directly
1883
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
1884
     *                        // for filter that only accept flags, you can also pass as an array
1885
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN,
1886
     *                        array('flags' => FILTER_NULL_ON_FAILURE));
1887
     *                        // callback validate filter
1888
     *                        function foo($value)
1889
     *                        {
1890
     *                        // Expected format: Surname, GivenNames
1891
     *                        if (strpos($value, ", ") === false) return false;
1892
     *                        list($surname, $givennames) = explode(", ", $value, 2);
1893
     *                        $empty = (empty($surname) || empty($givennames));
1894
     *                        $notstrings = (!is_string($surname) || !is_string($givennames));
1895
     *                        if ($empty || $notstrings) {
1896
     *                        return false;
1897
     *                        } else {
1898
     *                        return $value;
1899
     *                        }
1900
     *                        }
1901
     *                        $var = filter_var('Doe, Jane Sue', FILTER_CALLBACK, array('options' => 'foo'));
1902
     *                        </code>
1903
     *                        </p>
1904
     *
1905
     * @psalm-pure
1906
     *
1907
     * @return mixed the filtered data, or <b>FALSE</b> if the filter fails
1908
     */
1909 2
    public static function filter_var(
1910
        $variable,
1911
        int $filter = \FILTER_DEFAULT,
1912
        $options = null
1913
    ) {
1914
        /**
1915
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1916
         */
1917 2
        if (\func_num_args() < 3) {
1918 2
            $variable = \filter_var($variable, $filter);
1919
        } else {
1920 2
            $variable = \filter_var($variable, $filter, $options);
1921
        }
1922
1923 2
        return self::filter($variable);
1924
    }
1925
1926
    /**
1927
     * "filter_var_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1928
     *
1929
     * Gets multiple variables and optionally filters them
1930
     *
1931
     * @see http://php.net/manual/en/function.filter-var-array.php
1932
     *
1933
     * @param array<mixed> $data       <p>
1934
     *                                 An array with string keys containing the data to filter.
1935
     *                                 </p>
1936
     * @param mixed        $definition [optional] <p>
1937
     *                                 An array defining the arguments. A valid key is a string
1938
     *                                 containing a variable name and a valid value is either a
1939
     *                                 filter type, or an
1940
     *                                 array optionally specifying the filter, flags and options.
1941
     *                                 If the value is an array, valid keys are filter
1942
     *                                 which specifies the filter type,
1943
     *                                 flags which specifies any flags that apply to the
1944
     *                                 filter, and options which specifies any options that
1945
     *                                 apply to the filter. See the example below for a better understanding.
1946
     *                                 </p>
1947
     *                                 <p>
1948
     *                                 This parameter can be also an integer holding a filter constant. Then all values
1949
     *                                 in the input array are filtered by this filter.
1950
     *                                 </p>
1951
     * @param bool         $add_empty  [optional] <p>
1952
     *                                 Add missing keys as <b>NULL</b> to the return value.
1953
     *                                 </p>
1954
     *
1955
     * @psalm-pure
1956
     *
1957
     * @return mixed an array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1958
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1959
     *               set
1960
     */
1961 2
    public static function filter_var_array(
1962
        array $data,
1963
        $definition = null,
1964
        bool $add_empty = true
1965
    ) {
1966
        /**
1967
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1968
         */
1969 2
        if (\func_num_args() < 2) {
1970 2
            $a = \filter_var_array($data);
1971
        } else {
1972 2
            $a = \filter_var_array($data, $definition, $add_empty);
1973
        }
1974
1975 2
        return self::filter($a);
1976
    }
1977
1978
    /**
1979
     * Checks whether finfo is available on the server.
1980
     *
1981
     * @psalm-pure
1982
     *
1983
     * @return bool
1984
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
1985
     */
1986
    public static function finfo_loaded(): bool
1987
    {
1988
        return \class_exists('finfo');
1989
    }
1990
1991
    /**
1992
     * Returns the first $n characters of the string.
1993
     *
1994
     * @param string $str      <p>The input string.</p>
1995
     * @param int    $n        <p>Number of characters to retrieve from the start.</p>
1996
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
1997
     *
1998
     * @psalm-pure
1999
     *
2000
     * @return string
2001
     */
2002 13
    public static function first_char(
2003
        string $str,
2004
        int $n = 1,
2005
        string $encoding = 'UTF-8'
2006
    ): string {
2007 13
        if ($str === '' || $n <= 0) {
2008 5
            return '';
2009
        }
2010
2011 8
        if ($encoding === 'UTF-8') {
2012 4
            return (string) \mb_substr($str, 0, $n);
2013
        }
2014
2015 4
        return (string) self::substr($str, 0, $n, $encoding);
2016
    }
2017
2018
    /**
2019
     * Check if the number of Unicode characters isn't greater than the specified integer.
2020
     *
2021
     * @param string $str      the original string to be checked
2022
     * @param int    $box_size the size in number of chars to be checked against string
2023
     *
2024
     * @psalm-pure
2025
     *
2026
     * @return bool true if string is less than or equal to $box_size, false otherwise
2027
     */
2028 2
    public static function fits_inside(string $str, int $box_size): bool
2029
    {
2030 2
        return (int) self::strlen($str) <= $box_size;
2031
    }
2032
2033
    /**
2034
     * Try to fix simple broken UTF-8 strings.
2035
     *
2036
     * INFO: Take a look at "UTF8::fix_utf8()" if you need a more advanced fix for broken UTF-8 strings.
2037
     *
2038
     * If you received an UTF-8 string that was converted from Windows-1252 as it was ISO-8859-1
2039
     * (ignoring Windows-1252 chars from 80 to 9F) use this function to fix it.
2040
     * See: http://en.wikipedia.org/wiki/Windows-1252
2041
     *
2042
     * @param string $str <p>The input string</p>
2043
     *
2044
     * @psalm-pure
2045
     *
2046
     * @return string
2047
     */
2048 47
    public static function fix_simple_utf8(string $str): string
2049
    {
2050 47
        if ($str === '') {
2051 4
            return '';
2052
        }
2053
2054
        /**
2055
         * @psalm-suppress ImpureStaticVariable
2056
         *
2057
         * @var array<mixed>|null
2058
         */
2059 47
        static $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = null;
2060
2061
        /**
2062
         * @psalm-suppress ImpureStaticVariable
2063
         *
2064
         * @var array<mixed>|null
2065
         */
2066 47
        static $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = null;
2067
2068 47
        if ($BROKEN_UTF8_TO_UTF8_KEYS_CACHE === null) {
2069 1
            if (self::$BROKEN_UTF8_FIX === null) {
2070 1
                self::$BROKEN_UTF8_FIX = self::getData('utf8_fix');
2071
            }
2072
2073 1
            $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = \array_keys(self::$BROKEN_UTF8_FIX);
2074 1
            $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = self::$BROKEN_UTF8_FIX;
2075
        }
2076
2077 47
        \assert(\is_array($BROKEN_UTF8_TO_UTF8_VALUES_CACHE));
2078
2079 47
        return \str_replace($BROKEN_UTF8_TO_UTF8_KEYS_CACHE, $BROKEN_UTF8_TO_UTF8_VALUES_CACHE, $str);
2080
    }
2081
2082
    /**
2083
     * Fix a double (or multiple) encoded UTF8 string.
2084
     *
2085
     * @param string|string[] $str you can use a string or an array of strings
2086
     *
2087
     * @psalm-pure
2088
     *
2089
     * @return string|string[]
2090
     *                         Will return the fixed input-"array" or
2091
     *                         the fixed input-"string"
2092
     *
2093
     * @psalm-suppress InvalidReturnType
2094
     */
2095 2
    public static function fix_utf8($str)
2096
    {
2097 2
        if (\is_array($str) === true) {
2098 2
            foreach ($str as $k => &$v) {
2099 2
                $v = self::fix_utf8($v);
2100
            }
2101 2
            unset($v);
2102
2103
            /**
2104
             * @psalm-suppress InvalidReturnStatement
2105
             */
2106 2
            return $str;
2107
        }
2108
2109 2
        $str = (string) $str;
2110 2
        $last = '';
2111 2
        while ($last !== $str) {
2112 2
            $last = $str;
2113
            /**
2114
             * @psalm-suppress PossiblyInvalidArgument
2115
             */
2116 2
            $str = self::to_utf8(
2117 2
                self::utf8_decode($str, true)
2118
            );
2119
        }
2120
2121
        /**
2122
         * @psalm-suppress InvalidReturnStatement
2123
         */
2124 2
        return $str;
2125
    }
2126
2127
    /**
2128
     * Get character of a specific character.
2129
     *
2130
     * @param string $char
2131
     *
2132
     * @psalm-pure
2133
     *
2134
     * @return string 'RTL' or 'LTR'
2135
     */
2136 2
    public static function getCharDirection(string $char): string
2137
    {
2138 2
        if (self::$SUPPORT['intlChar'] === true) {
2139
            /** @noinspection PhpComposerExtensionStubsInspection */
2140 2
            $tmp_return = \IntlChar::charDirection($char);
2141
2142
            // from "IntlChar"-Class
2143
            $char_direction = [
2144 2
                'RTL' => [1, 13, 14, 15, 21],
2145
                'LTR' => [0, 11, 12, 20],
2146
            ];
2147
2148 2
            if (\in_array($tmp_return, $char_direction['LTR'], true)) {
2149
                return 'LTR';
2150
            }
2151
2152 2
            if (\in_array($tmp_return, $char_direction['RTL'], true)) {
2153 2
                return 'RTL';
2154
            }
2155
        }
2156
2157 2
        $c = static::chr_to_decimal($char);
2158
2159 2
        if (!($c >= 0x5be && $c <= 0x10b7f)) {
2160 2
            return 'LTR';
2161
        }
2162
2163 2
        if ($c <= 0x85e) {
2164 2
            if ($c === 0x5be ||
2165 2
                $c === 0x5c0 ||
2166 2
                $c === 0x5c3 ||
2167 2
                $c === 0x5c6 ||
2168 2
                ($c >= 0x5d0 && $c <= 0x5ea) ||
2169 2
                ($c >= 0x5f0 && $c <= 0x5f4) ||
2170 2
                $c === 0x608 ||
2171 2
                $c === 0x60b ||
2172 2
                $c === 0x60d ||
2173 2
                $c === 0x61b ||
2174 2
                ($c >= 0x61e && $c <= 0x64a) ||
2175
                ($c >= 0x66d && $c <= 0x66f) ||
2176
                ($c >= 0x671 && $c <= 0x6d5) ||
2177
                ($c >= 0x6e5 && $c <= 0x6e6) ||
2178
                ($c >= 0x6ee && $c <= 0x6ef) ||
2179
                ($c >= 0x6fa && $c <= 0x70d) ||
2180
                $c === 0x710 ||
2181
                ($c >= 0x712 && $c <= 0x72f) ||
2182
                ($c >= 0x74d && $c <= 0x7a5) ||
2183
                $c === 0x7b1 ||
2184
                ($c >= 0x7c0 && $c <= 0x7ea) ||
2185
                ($c >= 0x7f4 && $c <= 0x7f5) ||
2186
                $c === 0x7fa ||
2187
                ($c >= 0x800 && $c <= 0x815) ||
2188
                $c === 0x81a ||
2189
                $c === 0x824 ||
2190
                $c === 0x828 ||
2191
                ($c >= 0x830 && $c <= 0x83e) ||
2192
                ($c >= 0x840 && $c <= 0x858) ||
2193 2
                $c === 0x85e
2194
            ) {
2195 2
                return 'RTL';
2196
            }
2197 2
        } elseif ($c === 0x200f) {
2198
            return 'RTL';
2199 2
        } elseif ($c >= 0xfb1d) {
2200 2
            if ($c === 0xfb1d ||
2201 2
                ($c >= 0xfb1f && $c <= 0xfb28) ||
2202 2
                ($c >= 0xfb2a && $c <= 0xfb36) ||
2203 2
                ($c >= 0xfb38 && $c <= 0xfb3c) ||
2204 2
                $c === 0xfb3e ||
2205 2
                ($c >= 0xfb40 && $c <= 0xfb41) ||
2206 2
                ($c >= 0xfb43 && $c <= 0xfb44) ||
2207 2
                ($c >= 0xfb46 && $c <= 0xfbc1) ||
2208 2
                ($c >= 0xfbd3 && $c <= 0xfd3d) ||
2209 2
                ($c >= 0xfd50 && $c <= 0xfd8f) ||
2210 2
                ($c >= 0xfd92 && $c <= 0xfdc7) ||
2211 2
                ($c >= 0xfdf0 && $c <= 0xfdfc) ||
2212 2
                ($c >= 0xfe70 && $c <= 0xfe74) ||
2213 2
                ($c >= 0xfe76 && $c <= 0xfefc) ||
2214 2
                ($c >= 0x10800 && $c <= 0x10805) ||
2215 2
                $c === 0x10808 ||
2216 2
                ($c >= 0x1080a && $c <= 0x10835) ||
2217 2
                ($c >= 0x10837 && $c <= 0x10838) ||
2218 2
                $c === 0x1083c ||
2219 2
                ($c >= 0x1083f && $c <= 0x10855) ||
2220 2
                ($c >= 0x10857 && $c <= 0x1085f) ||
2221 2
                ($c >= 0x10900 && $c <= 0x1091b) ||
2222 2
                ($c >= 0x10920 && $c <= 0x10939) ||
2223 2
                $c === 0x1093f ||
2224 2
                $c === 0x10a00 ||
2225 2
                ($c >= 0x10a10 && $c <= 0x10a13) ||
2226 2
                ($c >= 0x10a15 && $c <= 0x10a17) ||
2227 2
                ($c >= 0x10a19 && $c <= 0x10a33) ||
2228 2
                ($c >= 0x10a40 && $c <= 0x10a47) ||
2229 2
                ($c >= 0x10a50 && $c <= 0x10a58) ||
2230 2
                ($c >= 0x10a60 && $c <= 0x10a7f) ||
2231 2
                ($c >= 0x10b00 && $c <= 0x10b35) ||
2232 2
                ($c >= 0x10b40 && $c <= 0x10b55) ||
2233 2
                ($c >= 0x10b58 && $c <= 0x10b72) ||
2234 2
                ($c >= 0x10b78 && $c <= 0x10b7f)
2235
            ) {
2236 2
                return 'RTL';
2237
            }
2238
        }
2239
2240 2
        return 'LTR';
2241
    }
2242
2243
    /**
2244
     * Check for php-support.
2245
     *
2246
     * @param string|null $key
2247
     *
2248
     * @psalm-pure
2249
     *
2250
     * @return mixed
2251
     *               Return the full support-"array", if $key === null<br>
2252
     *               return bool-value, if $key is used and available<br>
2253
     *               otherwise return <strong>null</strong>
2254
     */
2255 27
    public static function getSupportInfo(string $key = null)
2256
    {
2257 27
        if ($key === null) {
2258 4
            return self::$SUPPORT;
2259
        }
2260
2261 25
        if (self::$INTL_TRANSLITERATOR_LIST === null) {
2262 1
            self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
2263
        }
2264
        // compatibility fix for old versions
2265 25
        self::$SUPPORT['intl__transliterator_list_ids'] = self::$INTL_TRANSLITERATOR_LIST;
2266
2267 25
        return self::$SUPPORT[$key] ?? null;
2268
    }
2269
2270
    /**
2271
     * Warning: this method only works for some file-types (png, jpg)
2272
     *          if you need more supported types, please use e.g. "finfo"
2273
     *
2274
     * @param string $str
2275
     * @param array  $fallback <p>with this keys: 'ext', 'mime', 'type'
2276
     *
2277
     * @psalm-pure
2278
     *
2279
     * @return array<string, string|null>
2280
     *                       <p>with this keys: 'ext', 'mime', 'type'</p>
2281
     *
2282
     * @phpstan-param array{ext: null|string, mime: null|string, type: null|string} $fallback
2283
     */
2284 39
    public static function get_file_type(
2285
        string $str,
2286
        array $fallback = [
2287
            'ext'  => null,
2288
            'mime' => 'application/octet-stream',
2289
            'type' => null,
2290
        ]
2291
    ): array {
2292 39
        if ($str === '') {
2293
            return $fallback;
2294
        }
2295
2296
        /** @var false|string $str_info - needed for PhpStan (stubs error) */
2297 39
        $str_info = \substr($str, 0, 2);
2298 39
        if ($str_info === false || \strlen($str_info) !== 2) {
2299 11
            return $fallback;
2300
        }
2301
2302
        // DEBUG
2303
        //var_dump($str_info);
2304
2305 35
        $str_info = \unpack('C2chars', $str_info);
2306
2307
        /** @noinspection PhpSillyAssignmentInspection */
2308
        /** @var array|false $str_info - needed for PhpStan (stubs error) */
2309 35
        $str_info = $str_info;
2310
2311 35
        if ($str_info === false) {
2312
            return $fallback;
2313
        }
2314
        /** @noinspection OffsetOperationsInspection */
2315 35
        $type_code = (int) ($str_info['chars1'] . $str_info['chars2']);
2316
2317
        // DEBUG
2318
        //var_dump($type_code);
2319
2320
        //
2321
        // info: https://en.wikipedia.org/wiki/Magic_number_%28programming%29#Format_indicator
2322
        //
2323
        switch ($type_code) {
2324
            // WARNING: do not add too simple comparisons, because of false-positive results:
2325
            //
2326
            // 3780 => 'pdf', 7790 => 'exe', 7784 => 'midi', 8075 => 'zip',
2327
            // 8297 => 'rar', 7173 => 'gif', 7373 => 'tiff' 6677 => 'bmp', ...
2328
            //
2329 35
            case 255216:
2330
                $ext = 'jpg';
2331
                $mime = 'image/jpeg';
2332
                $type = 'binary';
2333
2334
                break;
2335 35
            case 13780:
2336 7
                $ext = 'png';
2337 7
                $mime = 'image/png';
2338 7
                $type = 'binary';
2339
2340 7
                break;
2341
            default:
2342 34
                return $fallback;
2343
        }
2344
2345
        return [
2346 7
            'ext'  => $ext,
2347 7
            'mime' => $mime,
2348 7
            'type' => $type,
2349
        ];
2350
    }
2351
2352
    /**
2353
     * @param int    $length         <p>Length of the random string.</p>
2354
     * @param string $possible_chars [optional] <p>Characters string for the random selection.</p>
2355
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
2356
     *
2357
     * @return string
2358
     */
2359 1
    public static function get_random_string(
2360
        int $length,
2361
        string $possible_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
2362
        string $encoding = 'UTF-8'
2363
    ): string {
2364
        // init
2365 1
        $i = 0;
2366 1
        $str = '';
2367
2368
        //
2369
        // add random chars
2370
        //
2371
2372 1
        if ($encoding === 'UTF-8') {
2373 1
            $max_length = (int) \mb_strlen($possible_chars);
2374 1
            if ($max_length === 0) {
2375 1
                return '';
2376
            }
2377
2378 1
            while ($i < $length) {
2379
                try {
2380 1
                    $rand_int = \random_int(0, $max_length - 1);
2381
                } catch (\Exception $e) {
2382
                    /** @noinspection RandomApiMigrationInspection */
2383
                    $rand_int = \mt_rand(0, $max_length - 1);
2384
                }
2385 1
                $char = \mb_substr($possible_chars, $rand_int, 1);
2386 1
                if ($char !== false) {
2387 1
                    $str .= $char;
2388 1
                    ++$i;
2389
                }
2390
            }
2391
        } else {
2392
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2393
2394
            $max_length = (int) self::strlen($possible_chars, $encoding);
2395
            if ($max_length === 0) {
2396
                return '';
2397
            }
2398
2399
            while ($i < $length) {
2400
                try {
2401
                    $rand_int = \random_int(0, $max_length - 1);
2402
                } catch (\Exception $e) {
2403
                    /** @noinspection RandomApiMigrationInspection */
2404
                    $rand_int = \mt_rand(0, $max_length - 1);
2405
                }
2406
                $char = self::substr($possible_chars, $rand_int, 1, $encoding);
2407
                if ($char !== false) {
2408
                    $str .= $char;
2409
                    ++$i;
2410
                }
2411
            }
2412
        }
2413
2414 1
        return $str;
2415
    }
2416
2417
    /**
2418
     * @param int|string $entropy_extra [optional] <p>Extra entropy via a string or int value.</p>
2419
     * @param bool       $use_md5       [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
2420
     *
2421
     * @return string
2422
     */
2423 1
    public static function get_unique_string($entropy_extra = '', bool $use_md5 = true): string
2424
    {
2425 1
        $unique_helper = \random_int(0, \mt_getrandmax()) .
2426 1
                         \session_id() .
2427 1
                         ($_SERVER['REMOTE_ADDR'] ?? '') .
2428 1
                         ($_SERVER['SERVER_ADDR'] ?? '') .
2429 1
                         $entropy_extra;
2430
2431 1
        $unique_string = \uniqid($unique_helper, true);
2432
2433 1
        if ($use_md5) {
2434 1
            $unique_string = \md5($unique_string . $unique_helper);
2435
        }
2436
2437 1
        return $unique_string;
2438
    }
2439
2440
    /**
2441
     * alias for "UTF8::string_has_bom()"
2442
     *
2443
     * @param string $str
2444
     *
2445
     * @psalm-pure
2446
     *
2447
     * @return bool
2448
     *
2449
     * @see        UTF8::string_has_bom()
2450
     * @deprecated <p>please use "UTF8::string_has_bom()"</p>
2451
     */
2452 2
    public static function hasBom(string $str): bool
2453
    {
2454 2
        return self::string_has_bom($str);
2455
    }
2456
2457
    /**
2458
     * Returns true if the string contains a lower case char, false otherwise.
2459
     *
2460
     * @param string $str <p>The input string.</p>
2461
     *
2462
     * @psalm-pure
2463
     *
2464
     * @return bool
2465
     *              <p>Whether or not the string contains a lower case character.</p>
2466
     */
2467 47
    public static function has_lowercase(string $str): bool
2468
    {
2469 47
        if (self::$SUPPORT['mbstring'] === true) {
2470
            /** @noinspection PhpComposerExtensionStubsInspection */
2471 47
            return \mb_ereg_match('.*[[:lower:]]', $str);
2472
        }
2473
2474
        return self::str_matches_pattern($str, '.*[[:lower:]]');
2475
    }
2476
2477
    /**
2478
     * Returns true if the string contains whitespace, false otherwise.
2479
     *
2480
     * @param string $str <p>The input string.</p>
2481
     *
2482
     * @psalm-pure
2483
     *
2484
     * @return bool
2485
     *              <p>Whether or not the string contains whitespace.</p>
2486
     */
2487 11
    public static function has_whitespace(string $str): bool
2488
    {
2489 11
        if (self::$SUPPORT['mbstring'] === true) {
2490
            /** @noinspection PhpComposerExtensionStubsInspection */
2491 11
            return \mb_ereg_match('.*[[:space:]]', $str);
2492
        }
2493
2494
        return self::str_matches_pattern($str, '.*[[:space:]]');
2495
    }
2496
2497
    /**
2498
     * Returns true if the string contains an upper case char, false otherwise.
2499
     *
2500
     * @param string $str <p>The input string.</p>
2501
     *
2502
     * @psalm-pure
2503
     *
2504
     * @return bool whether or not the string contains an upper case character
2505
     */
2506 12
    public static function has_uppercase(string $str): bool
2507
    {
2508 12
        if (self::$SUPPORT['mbstring'] === true) {
2509
            /** @noinspection PhpComposerExtensionStubsInspection */
2510 12
            return \mb_ereg_match('.*[[:upper:]]', $str);
2511
        }
2512
2513
        return self::str_matches_pattern($str, '.*[[:upper:]]');
2514
    }
2515
2516
    /**
2517
     * Converts a hexadecimal value into a UTF-8 character.
2518
     *
2519
     * @param string $hexdec <p>The hexadecimal value.</p>
2520
     *
2521
     * @psalm-pure
2522
     *
2523
     * @return false|string one single UTF-8 character
2524
     */
2525 4
    public static function hex_to_chr(string $hexdec)
2526
    {
2527 4
        return self::decimal_to_chr(\hexdec($hexdec));
2528
    }
2529
2530
    /**
2531
     * Converts hexadecimal U+xxxx code point representation to integer.
2532
     *
2533
     * INFO: opposite to UTF8::int_to_hex()
2534
     *
2535
     * @param string $hexdec <p>The hexadecimal code point representation.</p>
2536
     *
2537
     * @psalm-pure
2538
     *
2539
     * @return false|int the code point, or false on failure
2540
     */
2541 2
    public static function hex_to_int($hexdec)
2542
    {
2543
        // init
2544 2
        $hexdec = (string) $hexdec;
2545
2546 2
        if ($hexdec === '') {
2547 2
            return false;
2548
        }
2549
2550 2
        if (\preg_match('/^(?:\\\u|U\+|)([a-zA-Z0-9]{4,6})$/', $hexdec, $match)) {
2551 2
            return \intval($match[1], 16);
2552
        }
2553
2554 2
        return false;
2555
    }
2556
2557
    /**
2558
     * alias for "UTF8::html_entity_decode()"
2559
     *
2560
     * @param string $str
2561
     * @param int    $flags
2562
     * @param string $encoding
2563
     *
2564
     * @psalm-pure
2565
     *
2566
     * @return string
2567
     *
2568
     * @see        UTF8::html_entity_decode()
2569
     * @deprecated <p>please use "UTF8::html_entity_decode()"</p>
2570
     */
2571 2
    public static function html_decode(
2572
        string $str,
2573
        int $flags = null,
2574
        string $encoding = 'UTF-8'
2575
    ): string {
2576 2
        return self::html_entity_decode($str, $flags, $encoding);
2577
    }
2578
2579
    /**
2580
     * Converts a UTF-8 string to a series of HTML numbered entities.
2581
     *
2582
     * INFO: opposite to UTF8::html_decode()
2583
     *
2584
     * @param string $str              <p>The Unicode string to be encoded as numbered entities.</p>
2585
     * @param bool   $keep_ascii_chars [optional] <p>Keep ASCII chars.</p>
2586
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
2587
     *
2588
     * @psalm-pure
2589
     *
2590
     * @return string HTML numbered entities
2591
     */
2592 14
    public static function html_encode(
2593
        string $str,
2594
        bool $keep_ascii_chars = false,
2595
        string $encoding = 'UTF-8'
2596
    ): string {
2597 14
        if ($str === '') {
2598 4
            return '';
2599
        }
2600
2601 14
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2602 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2603
        }
2604
2605
        // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
2606 14
        if (self::$SUPPORT['mbstring'] === true) {
2607 14
            $start_code = 0x00;
2608 14
            if ($keep_ascii_chars === true) {
2609 13
                $start_code = 0x80;
2610
            }
2611
2612 14
            if ($encoding === 'UTF-8') {
2613
                /** @var false|string|null $return - needed for PhpStan (stubs error) */
2614 14
                $return = \mb_encode_numericentity(
2615 14
                    $str,
2616 14
                    [$start_code, 0xfffff, 0, 0xfffff, 0]
2617
                );
2618 14
                if ($return !== null && $return !== false) {
2619 14
                    return $return;
2620
                }
2621
            }
2622
2623
            /** @var false|string|null $return - needed for PhpStan (stubs error) */
2624 4
            $return = \mb_encode_numericentity(
2625 4
                $str,
2626 4
                [$start_code, 0xfffff, 0, 0xfffff, 0],
2627 4
                $encoding
2628
            );
2629 4
            if ($return !== null && $return !== false) {
2630 4
                return $return;
2631
            }
2632
        }
2633
2634
        //
2635
        // fallback via vanilla php
2636
        //
2637
2638
        return \implode(
2639
            '',
2640
            \array_map(
2641
                static function (string $chr) use ($keep_ascii_chars, $encoding): string {
2642
                    return self::single_chr_html_encode($chr, $keep_ascii_chars, $encoding);
2643
                },
2644
                self::str_split($str)
2645
            )
2646
        );
2647
    }
2648
2649
    /**
2650
     * UTF-8 version of html_entity_decode()
2651
     *
2652
     * The reason we are not using html_entity_decode() by itself is because
2653
     * while it is not technically correct to leave out the semicolon
2654
     * at the end of an entity most browsers will still interpret the entity
2655
     * correctly. html_entity_decode() does not convert entities without
2656
     * semicolons, so we are left with our own little solution here. Bummer.
2657
     *
2658
     * Convert all HTML entities to their applicable characters
2659
     *
2660
     * INFO: opposite to UTF8::html_encode()
2661
     *
2662
     * @see http://php.net/manual/en/function.html-entity-decode.php
2663
     *
2664
     * @param string $str      <p>
2665
     *                         The input string.
2666
     *                         </p>
2667
     * @param int    $flags    [optional] <p>
2668
     *                         A bitmask of one or more of the following flags, which specify how to handle quotes
2669
     *                         and which document type to use. The default is ENT_COMPAT | ENT_HTML401.
2670
     *                         <table>
2671
     *                         Available <i>flags</i> constants
2672
     *                         <tr valign="top">
2673
     *                         <td>Constant Name</td>
2674
     *                         <td>Description</td>
2675
     *                         </tr>
2676
     *                         <tr valign="top">
2677
     *                         <td><b>ENT_COMPAT</b></td>
2678
     *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
2679
     *                         </tr>
2680
     *                         <tr valign="top">
2681
     *                         <td><b>ENT_QUOTES</b></td>
2682
     *                         <td>Will convert both double and single quotes.</td>
2683
     *                         </tr>
2684
     *                         <tr valign="top">
2685
     *                         <td><b>ENT_NOQUOTES</b></td>
2686
     *                         <td>Will leave both double and single quotes unconverted.</td>
2687
     *                         </tr>
2688
     *                         <tr valign="top">
2689
     *                         <td><b>ENT_HTML401</b></td>
2690
     *                         <td>
2691
     *                         Handle code as HTML 4.01.
2692
     *                         </td>
2693
     *                         </tr>
2694
     *                         <tr valign="top">
2695
     *                         <td><b>ENT_XML1</b></td>
2696
     *                         <td>
2697
     *                         Handle code as XML 1.
2698
     *                         </td>
2699
     *                         </tr>
2700
     *                         <tr valign="top">
2701
     *                         <td><b>ENT_XHTML</b></td>
2702
     *                         <td>
2703
     *                         Handle code as XHTML.
2704
     *                         </td>
2705
     *                         </tr>
2706
     *                         <tr valign="top">
2707
     *                         <td><b>ENT_HTML5</b></td>
2708
     *                         <td>
2709
     *                         Handle code as HTML 5.
2710
     *                         </td>
2711
     *                         </tr>
2712
     *                         </table>
2713
     *                         </p>
2714
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2715
     *
2716
     * @psalm-pure
2717
     *
2718
     * @return string the decoded string
2719
     */
2720 51
    public static function html_entity_decode(
2721
        string $str,
2722
        int $flags = null,
2723
        string $encoding = 'UTF-8'
2724
    ): string {
2725
        if (
2726 51
            !isset($str[3]) // examples: &; || &x;
2727
            ||
2728 51
            \strpos($str, '&') === false // no "&"
2729
        ) {
2730 24
            return $str;
2731
        }
2732
2733 49
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2734 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2735
        }
2736
2737 49
        if ($flags === null) {
2738 11
            $flags = \ENT_QUOTES | \ENT_HTML5;
2739
        }
2740
2741
        if (
2742 49
            $encoding !== 'UTF-8'
2743
            &&
2744 49
            $encoding !== 'ISO-8859-1'
2745
            &&
2746 49
            $encoding !== 'WINDOWS-1252'
2747
            &&
2748 49
            self::$SUPPORT['mbstring'] === false
2749
        ) {
2750
            /**
2751
             * @psalm-suppress ImpureFunctionCall - is is only a warning
2752
             */
2753
            \trigger_error('UTF8::html_entity_decode() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
2754
        }
2755
2756
        do {
2757 49
            $str_compare = $str;
2758
2759 49
            if (\strpos($str, '&') !== false) {
2760 49
                if (\strpos($str, '&#') !== false) {
2761
                    // decode also numeric & UTF16 two byte entities
2762 41
                    $str = (string) \preg_replace(
2763 41
                        '/(&#(?:x0*[0-9a-fA-F]{2,6}(?![0-9a-fA-F;])|(?:0*\d{2,6}(?![0-9;]))))/S',
2764 41
                        '$1;',
2765 41
                        $str
2766
                    );
2767
                }
2768
2769 49
                $str = \html_entity_decode(
2770 49
                    $str,
2771 49
                    $flags,
2772 49
                    $encoding
2773
                );
2774
            }
2775 49
        } while ($str_compare !== $str);
2776
2777 49
        return $str;
2778
    }
2779
2780
    /**
2781
     * Create a escape html version of the string via "UTF8::htmlspecialchars()".
2782
     *
2783
     * @param string $str
2784
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2785
     *
2786
     * @psalm-pure
2787
     *
2788
     * @return string
2789
     */
2790 6
    public static function html_escape(string $str, string $encoding = 'UTF-8'): string
2791
    {
2792 6
        return self::htmlspecialchars(
2793 6
            $str,
2794 6
            \ENT_QUOTES | \ENT_SUBSTITUTE,
2795 6
            $encoding
2796
        );
2797
    }
2798
2799
    /**
2800
     * Remove empty html-tag.
2801
     *
2802
     * e.g.: <tag></tag>
2803
     *
2804
     * @param string $str
2805
     *
2806
     * @psalm-pure
2807
     *
2808
     * @return string
2809
     */
2810 1
    public static function html_stripe_empty_tags(string $str): string
2811
    {
2812 1
        return (string) \preg_replace(
2813 1
            '/<[^\\/>]*?>\\s*?<\\/[^>]*?>/u',
2814 1
            '',
2815 1
            $str
2816
        );
2817
    }
2818
2819
    /**
2820
     * Convert all applicable characters to HTML entities: UTF-8 version of htmlentities()
2821
     *
2822
     * @see http://php.net/manual/en/function.htmlentities.php
2823
     *
2824
     * @param string $str           <p>
2825
     *                              The input string.
2826
     *                              </p>
2827
     * @param int    $flags         [optional] <p>
2828
     *                              A bitmask of one or more of the following flags, which specify how to handle
2829
     *                              quotes, invalid code unit sequences and the used document type. The default is
2830
     *                              ENT_COMPAT | ENT_HTML401.
2831
     *                              <table>
2832
     *                              Available <i>flags</i> constants
2833
     *                              <tr valign="top">
2834
     *                              <td>Constant Name</td>
2835
     *                              <td>Description</td>
2836
     *                              </tr>
2837
     *                              <tr valign="top">
2838
     *                              <td><b>ENT_COMPAT</b></td>
2839
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2840
     *                              </tr>
2841
     *                              <tr valign="top">
2842
     *                              <td><b>ENT_QUOTES</b></td>
2843
     *                              <td>Will convert both double and single quotes.</td>
2844
     *                              </tr>
2845
     *                              <tr valign="top">
2846
     *                              <td><b>ENT_NOQUOTES</b></td>
2847
     *                              <td>Will leave both double and single quotes unconverted.</td>
2848
     *                              </tr>
2849
     *                              <tr valign="top">
2850
     *                              <td><b>ENT_IGNORE</b></td>
2851
     *                              <td>
2852
     *                              Silently discard invalid code unit sequences instead of returning
2853
     *                              an empty string. Using this flag is discouraged as it
2854
     *                              may have security implications.
2855
     *                              </td>
2856
     *                              </tr>
2857
     *                              <tr valign="top">
2858
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2859
     *                              <td>
2860
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
2861
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
2862
     *                              string.
2863
     *                              </td>
2864
     *                              </tr>
2865
     *                              <tr valign="top">
2866
     *                              <td><b>ENT_DISALLOWED</b></td>
2867
     *                              <td>
2868
     *                              Replace invalid code points for the given document type with a
2869
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
2870
     *                              (otherwise) instead of leaving them as is. This may be useful, for
2871
     *                              instance, to ensure the well-formedness of XML documents with
2872
     *                              embedded external content.
2873
     *                              </td>
2874
     *                              </tr>
2875
     *                              <tr valign="top">
2876
     *                              <td><b>ENT_HTML401</b></td>
2877
     *                              <td>
2878
     *                              Handle code as HTML 4.01.
2879
     *                              </td>
2880
     *                              </tr>
2881
     *                              <tr valign="top">
2882
     *                              <td><b>ENT_XML1</b></td>
2883
     *                              <td>
2884
     *                              Handle code as XML 1.
2885
     *                              </td>
2886
     *                              </tr>
2887
     *                              <tr valign="top">
2888
     *                              <td><b>ENT_XHTML</b></td>
2889
     *                              <td>
2890
     *                              Handle code as XHTML.
2891
     *                              </td>
2892
     *                              </tr>
2893
     *                              <tr valign="top">
2894
     *                              <td><b>ENT_HTML5</b></td>
2895
     *                              <td>
2896
     *                              Handle code as HTML 5.
2897
     *                              </td>
2898
     *                              </tr>
2899
     *                              </table>
2900
     *                              </p>
2901
     * @param string $encoding      [optional] <p>
2902
     *                              Like <b>htmlspecialchars</b>,
2903
     *                              <b>htmlentities</b> takes an optional third argument
2904
     *                              <i>encoding</i> which defines encoding used in
2905
     *                              conversion.
2906
     *                              Although this argument is technically optional, you are highly
2907
     *                              encouraged to specify the correct value for your code.
2908
     *                              </p>
2909
     * @param bool   $double_encode [optional] <p>
2910
     *                              When <i>double_encode</i> is turned off PHP will not
2911
     *                              encode existing html entities. The default is to convert everything.
2912
     *                              </p>
2913
     *
2914
     * @psalm-pure
2915
     *
2916
     * @return string
2917
     *                <p>
2918
     *                The encoded string.
2919
     *                <br><br>
2920
     *                If the input <i>string</i> contains an invalid code unit
2921
     *                sequence within the given <i>encoding</i> an empty string
2922
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
2923
     *                <b>ENT_SUBSTITUTE</b> flags are set.
2924
     *                </p>
2925
     */
2926 9
    public static function htmlentities(
2927
        string $str,
2928
        int $flags = \ENT_COMPAT,
2929
        string $encoding = 'UTF-8',
2930
        bool $double_encode = true
2931
    ): string {
2932 9
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2933 7
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2934
        }
2935
2936 9
        $str = \htmlentities(
2937 9
            $str,
2938 9
            $flags,
2939 9
            $encoding,
2940 9
            $double_encode
2941
        );
2942
2943
        /**
2944
         * PHP doesn't replace a backslash to its html entity since this is something
2945
         * that's mostly used to escape characters when inserting in a database. Since
2946
         * we're using a decent database layer, we don't need this shit and we're replacing
2947
         * the double backslashes by its' html entity equivalent.
2948
         *
2949
         * https://github.com/forkcms/library/blob/master/spoon/filter/filter.php#L303
2950
         */
2951 9
        $str = \str_replace('\\', '&#92;', $str);
2952
2953 9
        return self::html_encode($str, true, $encoding);
2954
    }
2955
2956
    /**
2957
     * Convert only special characters to HTML entities: UTF-8 version of htmlspecialchars()
2958
     *
2959
     * INFO: Take a look at "UTF8::htmlentities()"
2960
     *
2961
     * @see http://php.net/manual/en/function.htmlspecialchars.php
2962
     *
2963
     * @param string $str           <p>
2964
     *                              The string being converted.
2965
     *                              </p>
2966
     * @param int    $flags         [optional] <p>
2967
     *                              A bitmask of one or more of the following flags, which specify how to handle
2968
     *                              quotes, invalid code unit sequences and the used document type. The default is
2969
     *                              ENT_COMPAT | ENT_HTML401.
2970
     *                              <table>
2971
     *                              Available <i>flags</i> constants
2972
     *                              <tr valign="top">
2973
     *                              <td>Constant Name</td>
2974
     *                              <td>Description</td>
2975
     *                              </tr>
2976
     *                              <tr valign="top">
2977
     *                              <td><b>ENT_COMPAT</b></td>
2978
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2979
     *                              </tr>
2980
     *                              <tr valign="top">
2981
     *                              <td><b>ENT_QUOTES</b></td>
2982
     *                              <td>Will convert both double and single quotes.</td>
2983
     *                              </tr>
2984
     *                              <tr valign="top">
2985
     *                              <td><b>ENT_NOQUOTES</b></td>
2986
     *                              <td>Will leave both double and single quotes unconverted.</td>
2987
     *                              </tr>
2988
     *                              <tr valign="top">
2989
     *                              <td><b>ENT_IGNORE</b></td>
2990
     *                              <td>
2991
     *                              Silently discard invalid code unit sequences instead of returning
2992
     *                              an empty string. Using this flag is discouraged as it
2993
     *                              may have security implications.
2994
     *                              </td>
2995
     *                              </tr>
2996
     *                              <tr valign="top">
2997
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2998
     *                              <td>
2999
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
3000
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
3001
     *                              string.
3002
     *                              </td>
3003
     *                              </tr>
3004
     *                              <tr valign="top">
3005
     *                              <td><b>ENT_DISALLOWED</b></td>
3006
     *                              <td>
3007
     *                              Replace invalid code points for the given document type with a
3008
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
3009
     *                              (otherwise) instead of leaving them as is. This may be useful, for
3010
     *                              instance, to ensure the well-formedness of XML documents with
3011
     *                              embedded external content.
3012
     *                              </td>
3013
     *                              </tr>
3014
     *                              <tr valign="top">
3015
     *                              <td><b>ENT_HTML401</b></td>
3016
     *                              <td>
3017
     *                              Handle code as HTML 4.01.
3018
     *                              </td>
3019
     *                              </tr>
3020
     *                              <tr valign="top">
3021
     *                              <td><b>ENT_XML1</b></td>
3022
     *                              <td>
3023
     *                              Handle code as XML 1.
3024
     *                              </td>
3025
     *                              </tr>
3026
     *                              <tr valign="top">
3027
     *                              <td><b>ENT_XHTML</b></td>
3028
     *                              <td>
3029
     *                              Handle code as XHTML.
3030
     *                              </td>
3031
     *                              </tr>
3032
     *                              <tr valign="top">
3033
     *                              <td><b>ENT_HTML5</b></td>
3034
     *                              <td>
3035
     *                              Handle code as HTML 5.
3036
     *                              </td>
3037
     *                              </tr>
3038
     *                              </table>
3039
     *                              </p>
3040
     * @param string $encoding      [optional] <p>
3041
     *                              Defines encoding used in conversion.
3042
     *                              </p>
3043
     *                              <p>
3044
     *                              For the purposes of this function, the encodings
3045
     *                              ISO-8859-1, ISO-8859-15,
3046
     *                              UTF-8, cp866,
3047
     *                              cp1251, cp1252, and
3048
     *                              KOI8-R are effectively equivalent, provided the
3049
     *                              <i>string</i> itself is valid for the encoding, as
3050
     *                              the characters affected by <b>htmlspecialchars</b> occupy
3051
     *                              the same positions in all of these encodings.
3052
     *                              </p>
3053
     * @param bool   $double_encode [optional] <p>
3054
     *                              When <i>double_encode</i> is turned off PHP will not
3055
     *                              encode existing html entities, the default is to convert everything.
3056
     *                              </p>
3057
     *
3058
     * @psalm-pure
3059
     *
3060
     * @return string the converted string.
3061
     *                </p>
3062
     *                <p>
3063
     *                If the input <i>string</i> contains an invalid code unit
3064
     *                sequence within the given <i>encoding</i> an empty string
3065
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
3066
     *                <b>ENT_SUBSTITUTE</b> flags are set
3067
     */
3068 8
    public static function htmlspecialchars(
3069
        string $str,
3070
        int $flags = \ENT_COMPAT,
3071
        string $encoding = 'UTF-8',
3072
        bool $double_encode = true
3073
    ): string {
3074 8
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
3075 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
3076
        }
3077
3078 8
        return \htmlspecialchars(
3079 8
            $str,
3080 8
            $flags,
3081 8
            $encoding,
3082 8
            $double_encode
3083
        );
3084
    }
3085
3086
    /**
3087
     * Checks whether iconv is available on the server.
3088
     *
3089
     * @psalm-pure
3090
     *
3091
     * @return bool
3092
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3093
     */
3094
    public static function iconv_loaded(): bool
3095
    {
3096
        return \extension_loaded('iconv');
3097
    }
3098
3099
    /**
3100
     * alias for "UTF8::decimal_to_chr()"
3101
     *
3102
     * @param mixed $int
3103
     *
3104
     * @psalm-pure
3105
     *
3106
     * @return string
3107
     *
3108
     * @see        UTF8::decimal_to_chr()
3109
     * @deprecated <p>please use "UTF8::decimal_to_chr()"</p>
3110
     */
3111 4
    public static function int_to_chr($int): string
3112
    {
3113 4
        return self::decimal_to_chr($int);
3114
    }
3115
3116
    /**
3117
     * Converts Integer to hexadecimal U+xxxx code point representation.
3118
     *
3119
     * INFO: opposite to UTF8::hex_to_int()
3120
     *
3121
     * @param int    $int    <p>The integer to be converted to hexadecimal code point.</p>
3122
     * @param string $prefix [optional]
3123
     *
3124
     * @psalm-pure
3125
     *
3126
     * @return string the code point, or empty string on failure
3127
     */
3128 6
    public static function int_to_hex(int $int, string $prefix = 'U+'): string
3129
    {
3130 6
        $hex = \dechex($int);
3131
3132 6
        $hex = (\strlen($hex) < 4 ? \substr('0000' . $hex, -4) : $hex);
3133
3134 6
        return $prefix . $hex . '';
3135
    }
3136
3137
    /**
3138
     * Checks whether intl-char is available on the server.
3139
     *
3140
     * @psalm-pure
3141
     *
3142
     * @return bool
3143
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3144
     */
3145
    public static function intlChar_loaded(): bool
3146
    {
3147
        return \class_exists('IntlChar');
3148
    }
3149
3150
    /**
3151
     * Checks whether intl is available on the server.
3152
     *
3153
     * @psalm-pure
3154
     *
3155
     * @return bool
3156
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3157
     */
3158 5
    public static function intl_loaded(): bool
3159
    {
3160 5
        return \extension_loaded('intl');
3161
    }
3162
3163
    /**
3164
     * alias for "UTF8::is_ascii()"
3165
     *
3166
     * @param string $str
3167
     *
3168
     * @psalm-pure
3169
     *
3170
     * @return bool
3171
     *
3172
     * @see        UTF8::is_ascii()
3173
     * @deprecated <p>please use "UTF8::is_ascii()"</p>
3174
     */
3175 2
    public static function isAscii(string $str): bool
3176
    {
3177 2
        return ASCII::is_ascii($str);
3178
    }
3179
3180
    /**
3181
     * alias for "UTF8::is_base64()"
3182
     *
3183
     * @param string $str
3184
     *
3185
     * @psalm-pure
3186
     *
3187
     * @return bool
3188
     *
3189
     * @see        UTF8::is_base64()
3190
     * @deprecated <p>please use "UTF8::is_base64()"</p>
3191
     */
3192 2
    public static function isBase64($str): bool
3193
    {
3194 2
        return self::is_base64($str);
3195
    }
3196
3197
    /**
3198
     * alias for "UTF8::is_binary()"
3199
     *
3200
     * @param mixed $str
3201
     * @param bool  $strict
3202
     *
3203
     * @psalm-pure
3204
     *
3205
     * @return bool
3206
     *
3207
     * @see        UTF8::is_binary()
3208
     * @deprecated <p>please use "UTF8::is_binary()"</p>
3209
     */
3210 4
    public static function isBinary($str, $strict = false): bool
3211
    {
3212 4
        return self::is_binary($str, $strict);
3213
    }
3214
3215
    /**
3216
     * alias for "UTF8::is_bom()"
3217
     *
3218
     * @param string $utf8_chr
3219
     *
3220
     * @psalm-pure
3221
     *
3222
     * @return bool
3223
     *
3224
     * @see        UTF8::is_bom()
3225
     * @deprecated <p>please use "UTF8::is_bom()"</p>
3226
     */
3227 2
    public static function isBom(string $utf8_chr): bool
3228
    {
3229 2
        return self::is_bom($utf8_chr);
3230
    }
3231
3232
    /**
3233
     * alias for "UTF8::is_html()"
3234
     *
3235
     * @param string $str
3236
     *
3237
     * @psalm-pure
3238
     *
3239
     * @return bool
3240
     *
3241
     * @see        UTF8::is_html()
3242
     * @deprecated <p>please use "UTF8::is_html()"</p>
3243
     */
3244 2
    public static function isHtml(string $str): bool
3245
    {
3246 2
        return self::is_html($str);
3247
    }
3248
3249
    /**
3250
     * alias for "UTF8::is_json()"
3251
     *
3252
     * @param string $str
3253
     *
3254
     * @return bool
3255
     *
3256
     * @see        UTF8::is_json()
3257
     * @deprecated <p>please use "UTF8::is_json()"</p>
3258
     */
3259
    public static function isJson(string $str): bool
3260
    {
3261
        return self::is_json($str);
3262
    }
3263
3264
    /**
3265
     * alias for "UTF8::is_utf16()"
3266
     *
3267
     * @param mixed $str
3268
     *
3269
     * @psalm-pure
3270
     *
3271
     * @return false|int
3272
     *                   <strong>false</strong> if is't not UTF16,<br>
3273
     *                   <strong>1</strong> for UTF-16LE,<br>
3274
     *                   <strong>2</strong> for UTF-16BE
3275
     *
3276
     * @see        UTF8::is_utf16()
3277
     * @deprecated <p>please use "UTF8::is_utf16()"</p>
3278
     */
3279 2
    public static function isUtf16($str)
3280
    {
3281 2
        return self::is_utf16($str);
3282
    }
3283
3284
    /**
3285
     * alias for "UTF8::is_utf32()"
3286
     *
3287
     * @param mixed $str
3288
     *
3289
     * @psalm-pure
3290
     *
3291
     * @return false|int
3292
     *                   <strong>false</strong> if is't not UTF16,
3293
     *                   <strong>1</strong> for UTF-32LE,
3294
     *                   <strong>2</strong> for UTF-32BE
3295
     *
3296
     * @see        UTF8::is_utf32()
3297
     * @deprecated <p>please use "UTF8::is_utf32()"</p>
3298
     */
3299 2
    public static function isUtf32($str)
3300
    {
3301 2
        return self::is_utf32($str);
3302
    }
3303
3304
    /**
3305
     * alias for "UTF8::is_utf8()"
3306
     *
3307
     * @param string $str
3308
     * @param bool   $strict
3309
     *
3310
     * @psalm-pure
3311
     *
3312
     * @return bool
3313
     *
3314
     * @see        UTF8::is_utf8()
3315
     * @deprecated <p>please use "UTF8::is_utf8()"</p>
3316
     */
3317 17
    public static function isUtf8($str, $strict = false): bool
3318
    {
3319 17
        return self::is_utf8($str, $strict);
3320
    }
3321
3322
    /**
3323
     * Returns true if the string contains only alphabetic chars, false otherwise.
3324
     *
3325
     * @param string $str <p>The input string.</p>
3326
     *
3327
     * @psalm-pure
3328
     *
3329
     * @return bool
3330
     *              <p>Whether or not $str contains only alphabetic chars.</p>
3331
     */
3332 10
    public static function is_alpha(string $str): bool
3333
    {
3334 10
        if (self::$SUPPORT['mbstring'] === true) {
3335
            /** @noinspection PhpComposerExtensionStubsInspection */
3336 10
            return \mb_ereg_match('^[[:alpha:]]*$', $str);
3337
        }
3338
3339
        return self::str_matches_pattern($str, '^[[:alpha:]]*$');
3340
    }
3341
3342
    /**
3343
     * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
3344
     *
3345
     * @param string $str <p>The input string.</p>
3346
     *
3347
     * @psalm-pure
3348
     *
3349
     * @return bool
3350
     *              <p>Whether or not $str contains only alphanumeric chars.</p>
3351
     */
3352 13
    public static function is_alphanumeric(string $str): bool
3353
    {
3354 13
        if (self::$SUPPORT['mbstring'] === true) {
3355
            /** @noinspection PhpComposerExtensionStubsInspection */
3356 13
            return \mb_ereg_match('^[[:alnum:]]*$', $str);
3357
        }
3358
3359
        return self::str_matches_pattern($str, '^[[:alnum:]]*$');
3360
    }
3361
3362
    /**
3363
     * Returns true if the string contains only punctuation chars, false otherwise.
3364
     *
3365
     * @param string $str <p>The input string.</p>
3366
     *
3367
     * @psalm-pure
3368
     *
3369
     * @return bool
3370
     *              <p>Whether or not $str contains only punctuation chars.</p>
3371
     */
3372 10
    public static function is_punctuation(string $str): bool
3373
    {
3374 10
        return self::str_matches_pattern($str, '^[[:punct:]]*$');
3375
    }
3376
3377
    /**
3378
     * Returns true if the string contains only printable (non-invisible) chars, false otherwise.
3379
     *
3380
     * @param string $str <p>The input string.</p>
3381
     *
3382
     * @psalm-pure
3383
     *
3384
     * @return bool
3385
     *              <p>Whether or not $str contains only printable (non-invisible) chars.</p>
3386
     */
3387 1
    public static function is_printable(string $str): bool
3388
    {
3389 1
        return self::remove_invisible_characters($str) === $str;
3390
    }
3391
3392
    /**
3393
     * Checks if a string is 7 bit ASCII.
3394
     *
3395
     * @param string $str <p>The string to check.</p>
3396
     *
3397
     * @psalm-pure
3398
     *
3399
     * @return bool
3400
     *              <p>
3401
     *              <strong>true</strong> if it is ASCII<br>
3402
     *              <strong>false</strong> otherwise
3403
     *              </p>
3404
     */
3405 8
    public static function is_ascii(string $str): bool
3406
    {
3407 8
        return ASCII::is_ascii($str);
3408
    }
3409
3410
    /**
3411
     * Returns true if the string is base64 encoded, false otherwise.
3412
     *
3413
     * @param mixed|string $str                   <p>The input string.</p>
3414
     * @param bool         $empty_string_is_valid [optional] <p>Is an empty string valid base64 or not?</p>
3415
     *
3416
     * @psalm-pure
3417
     *
3418
     * @return bool whether or not $str is base64 encoded
3419
     */
3420 16
    public static function is_base64($str, $empty_string_is_valid = false): bool
3421
    {
3422
        if (
3423 16
            $empty_string_is_valid === false
3424
            &&
3425 16
            $str === ''
3426
        ) {
3427 3
            return false;
3428
        }
3429
3430
        /**
3431
         * @psalm-suppress RedundantConditionGivenDocblockType
3432
         */
3433 15
        if (\is_string($str) === false) {
3434 2
            return false;
3435
        }
3436
3437 15
        $base64String = \base64_decode($str, true);
3438
3439 15
        return $base64String !== false && \base64_encode($base64String) === $str;
3440
    }
3441
3442
    /**
3443
     * Check if the input is binary... (is look like a hack).
3444
     *
3445
     * @param mixed $input
3446
     * @param bool  $strict
3447
     *
3448
     * @psalm-pure
3449
     *
3450
     * @return bool
3451
     */
3452 39
    public static function is_binary($input, bool $strict = false): bool
3453
    {
3454 39
        $input = (string) $input;
3455 39
        if ($input === '') {
3456 10
            return false;
3457
        }
3458
3459 39
        if (\preg_match('~^[01]+$~', $input)) {
3460 13
            return true;
3461
        }
3462
3463 39
        $ext = self::get_file_type($input);
3464 39
        if ($ext['type'] === 'binary') {
3465 7
            return true;
3466
        }
3467
3468 38
        $test_length = \strlen($input);
3469 38
        $test_null_counting = \substr_count($input, "\x0", 0, $test_length);
3470 38
        if (($test_null_counting / $test_length) > 0.25) {
3471 15
            return true;
3472
        }
3473
3474 34
        if ($strict === true) {
3475 34
            if (self::$SUPPORT['finfo'] === false) {
3476
                throw new \RuntimeException('ext-fileinfo: is not installed');
3477
            }
3478
3479
            /**
3480
             * @noinspection   PhpComposerExtensionStubsInspection
3481
             * @psalm-suppress ImpureMethodCall - it will return the same result for the same file ...
3482
             */
3483 34
            $finfo_encoding = (new \finfo(\FILEINFO_MIME_ENCODING))->buffer($input);
3484 34
            if ($finfo_encoding && $finfo_encoding === 'binary') {
3485 15
                return true;
3486
            }
3487
        }
3488
3489 30
        return false;
3490
    }
3491
3492
    /**
3493
     * Check if the file is binary.
3494
     *
3495
     * @param string $file
3496
     *
3497
     * @return bool
3498
     */
3499 6
    public static function is_binary_file($file): bool
3500
    {
3501
        // init
3502 6
        $block = '';
3503
3504 6
        $fp = \fopen($file, 'rb');
3505 6
        if (\is_resource($fp)) {
3506 6
            $block = \fread($fp, 512);
3507 6
            \fclose($fp);
3508
        }
3509
3510 6
        if ($block === '') {
3511 2
            return false;
3512
        }
3513
3514 6
        return self::is_binary($block, true);
3515
    }
3516
3517
    /**
3518
     * Returns true if the string contains only whitespace chars, false otherwise.
3519
     *
3520
     * @param string $str <p>The input string.</p>
3521
     *
3522
     * @psalm-pure
3523
     *
3524
     * @return bool
3525
     *              <p>Whether or not $str contains only whitespace characters.</p>
3526
     */
3527 15
    public static function is_blank(string $str): bool
3528
    {
3529 15
        if (self::$SUPPORT['mbstring'] === true) {
3530
            /** @noinspection PhpComposerExtensionStubsInspection */
3531 15
            return \mb_ereg_match('^[[:space:]]*$', $str);
3532
        }
3533
3534
        return self::str_matches_pattern($str, '^[[:space:]]*$');
3535
    }
3536
3537
    /**
3538
     * Checks if the given string is equal to any "Byte Order Mark".
3539
     *
3540
     * WARNING: Use "UTF8::string_has_bom()" if you will check BOM in a string.
3541
     *
3542
     * @param string $str <p>The input string.</p>
3543
     *
3544
     * @psalm-pure
3545
     *
3546
     * @return bool
3547
     *              <p><strong>true</strong> if the $utf8_chr is Byte Order Mark, <strong>false</strong> otherwise.</p>
3548
     */
3549 2
    public static function is_bom($str): bool
3550
    {
3551
        /** @noinspection PhpUnusedLocalVariableInspection */
3552 2
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
3553 2
            if ($str === $bom_string) {
3554 2
                return true;
3555
            }
3556
        }
3557
3558 2
        return false;
3559
    }
3560
3561
    /**
3562
     * Determine whether the string is considered to be empty.
3563
     *
3564
     * A variable is considered empty if it does not exist or if its value equals FALSE.
3565
     * empty() does not generate a warning if the variable does not exist.
3566
     *
3567
     * @param mixed $str
3568
     *
3569
     * @psalm-pure
3570
     *
3571
     * @return bool whether or not $str is empty()
3572
     */
3573
    public static function is_empty($str): bool
3574
    {
3575
        return empty($str);
3576
    }
3577
3578
    /**
3579
     * Returns true if the string contains only hexadecimal chars, false otherwise.
3580
     *
3581
     * @param string $str <p>The input string.</p>
3582
     *
3583
     * @psalm-pure
3584
     *
3585
     * @return bool
3586
     *              <p>Whether or not $str contains only hexadecimal chars.</p>
3587
     */
3588 13
    public static function is_hexadecimal(string $str): bool
3589
    {
3590 13
        if (self::$SUPPORT['mbstring'] === true) {
3591
            /** @noinspection PhpComposerExtensionStubsInspection */
3592 13
            return \mb_ereg_match('^[[:xdigit:]]*$', $str);
3593
        }
3594
3595
        return self::str_matches_pattern($str, '^[[:xdigit:]]*$');
3596
    }
3597
3598
    /**
3599
     * Check if the string contains any HTML tags.
3600
     *
3601
     * @param string $str <p>The input string.</p>
3602
     *
3603
     * @psalm-pure
3604
     *
3605
     * @return bool
3606
     *              <p>Whether or not $str contains html elements.</p>
3607
     */
3608 3
    public static function is_html(string $str): bool
3609
    {
3610 3
        if ($str === '') {
3611 3
            return false;
3612
        }
3613
3614
        // init
3615 3
        $matches = [];
3616
3617 3
        $str = self::emoji_encode($str); // hack for emoji support :/
3618
3619 3
        \preg_match("/<\\/?\\w+(?:(?:\\s+\\w+(?:\\s*=\\s*(?:\".*?\"|'.*?'|[^'\">\\s]+))?)*\\s*|\\s*)\\/?>/u", $str, $matches);
3620
3621 3
        return $matches !== [];
3622
    }
3623
3624
    /**
3625
     * Try to check if "$str" is a JSON-string.
3626
     *
3627
     * @param string $str                                    <p>The input string.</p>
3628
     * @param bool   $only_array_or_object_results_are_valid [optional] <p>Only array and objects are valid json
3629
     *                                                       results.</p>
3630
     *
3631
     * @return bool
3632
     *              <p>Whether or not the $str is in JSON format.</p>
3633
     */
3634 42
    public static function is_json(
3635
        string $str,
3636
        $only_array_or_object_results_are_valid = true
3637
    ): bool {
3638 42
        if ($str === '') {
3639 4
            return false;
3640
        }
3641
3642 40
        if (self::$SUPPORT['json'] === false) {
3643
            throw new \RuntimeException('ext-json: is not installed');
3644
        }
3645
3646 40
        $json = self::json_decode($str);
3647 40
        if ($json === null && \strtoupper($str) !== 'NULL') {
3648 18
            return false;
3649
        }
3650
3651
        if (
3652 24
            $only_array_or_object_results_are_valid === true
3653
            &&
3654 24
            \is_object($json) === false
3655
            &&
3656 24
            \is_array($json) === false
3657
        ) {
3658 5
            return false;
3659
        }
3660
3661
        /** @noinspection PhpComposerExtensionStubsInspection */
3662 19
        return \json_last_error() === \JSON_ERROR_NONE;
3663
    }
3664
3665
    /**
3666
     * @param string $str <p>The input string.</p>
3667
     *
3668
     * @psalm-pure
3669
     *
3670
     * @return bool
3671
     *              <p>Whether or not $str contains only lowercase chars.</p>
3672
     */
3673 8
    public static function is_lowercase(string $str): bool
3674
    {
3675 8
        if (self::$SUPPORT['mbstring'] === true) {
3676
            /** @noinspection PhpComposerExtensionStubsInspection */
3677 8
            return \mb_ereg_match('^[[:lower:]]*$', $str);
3678
        }
3679
3680
        return self::str_matches_pattern($str, '^[[:lower:]]*$');
3681
    }
3682
3683
    /**
3684
     * Returns true if the string is serialized, false otherwise.
3685
     *
3686
     * @param string $str <p>The input string.</p>
3687
     *
3688
     * @psalm-pure
3689
     *
3690
     * @return bool
3691
     *              <p>Whether or not $str is serialized.</p>
3692
     */
3693 7
    public static function is_serialized(string $str): bool
3694
    {
3695 7
        if ($str === '') {
3696 1
            return false;
3697
        }
3698
3699
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
3700
        /** @noinspection UnserializeExploitsInspection */
3701 6
        return $str === 'b:0;'
3702
               ||
3703 6
               @\unserialize($str) !== false;
3704
    }
3705
3706
    /**
3707
     * Returns true if the string contains only lower case chars, false
3708
     * otherwise.
3709
     *
3710
     * @param string $str <p>The input string.</p>
3711
     *
3712
     * @psalm-pure
3713
     *
3714
     * @return bool
3715
     *              <p>Whether or not $str contains only lower case characters.</p>
3716
     */
3717 8
    public static function is_uppercase(string $str): bool
3718
    {
3719 8
        if (self::$SUPPORT['mbstring'] === true) {
3720
            /** @noinspection PhpComposerExtensionStubsInspection */
3721 8
            return \mb_ereg_match('^[[:upper:]]*$', $str);
3722
        }
3723
3724
        return self::str_matches_pattern($str, '^[[:upper:]]*$');
3725
    }
3726
3727
    /**
3728
     * Check if the string is UTF-16.
3729
     *
3730
     * @param mixed $str                       <p>The input string.</p>
3731
     * @param bool  $check_if_string_is_binary
3732
     *
3733
     * @psalm-pure
3734
     *
3735
     * @return false|int
3736
     *                   <strong>false</strong> if is't not UTF-16,<br>
3737
     *                   <strong>1</strong> for UTF-16LE,<br>
3738
     *                   <strong>2</strong> for UTF-16BE
3739
     */
3740 22
    public static function is_utf16($str, $check_if_string_is_binary = true)
3741
    {
3742
        // init
3743 22
        $str = (string) $str;
3744 22
        $str_chars = [];
3745
3746
        if (
3747 22
            $check_if_string_is_binary === true
3748
            &&
3749 22
            self::is_binary($str, true) === false
3750
        ) {
3751 2
            return false;
3752
        }
3753
3754 22
        if (self::$SUPPORT['mbstring'] === false) {
3755
            /**
3756
             * @psalm-suppress ImpureFunctionCall - is is only a warning
3757
             */
3758 3
            \trigger_error('UTF8::is_utf16() without mbstring may did not work correctly', \E_USER_WARNING);
3759
        }
3760
3761 22
        $str = self::remove_bom($str);
3762
3763 22
        $maybe_utf16le = 0;
3764 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16LE');
3765 22
        if ($test) {
3766 15
            $test2 = \mb_convert_encoding($test, 'UTF-16LE', 'UTF-8');
3767 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16LE');
3768 15
            if ($test3 === $test) {
3769
                /**
3770
                 * @psalm-suppress RedundantCondition
3771
                 */
3772 15
                if ($str_chars === []) {
3773 15
                    $str_chars = self::count_chars($str, true, false);
3774
                }
3775 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...
3776 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3777 15
                        ++$maybe_utf16le;
3778
                    }
3779
                }
3780 15
                unset($test3charEmpty);
3781
            }
3782
        }
3783
3784 22
        $maybe_utf16be = 0;
3785 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16BE');
3786 22
        if ($test) {
3787 15
            $test2 = \mb_convert_encoding($test, 'UTF-16BE', 'UTF-8');
3788 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16BE');
3789 15
            if ($test3 === $test) {
3790 15
                if ($str_chars === []) {
3791 7
                    $str_chars = self::count_chars($str, true, false);
3792
                }
3793 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...
3794 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3795 15
                        ++$maybe_utf16be;
3796
                    }
3797
                }
3798 15
                unset($test3charEmpty);
3799
            }
3800
        }
3801
3802 22
        if ($maybe_utf16be !== $maybe_utf16le) {
3803 7
            if ($maybe_utf16le > $maybe_utf16be) {
3804 5
                return 1;
3805
            }
3806
3807 6
            return 2;
3808
        }
3809
3810 18
        return false;
3811
    }
3812
3813
    /**
3814
     * Check if the string is UTF-32.
3815
     *
3816
     * @param mixed $str                       <p>The input string.</p>
3817
     * @param bool  $check_if_string_is_binary
3818
     *
3819
     * @psalm-pure
3820
     *
3821
     * @return false|int
3822
     *                   <strong>false</strong> if is't not UTF-32,<br>
3823
     *                   <strong>1</strong> for UTF-32LE,<br>
3824
     *                   <strong>2</strong> for UTF-32BE
3825
     */
3826 20
    public static function is_utf32($str, $check_if_string_is_binary = true)
3827
    {
3828
        // init
3829 20
        $str = (string) $str;
3830 20
        $str_chars = [];
3831
3832
        if (
3833 20
            $check_if_string_is_binary === true
3834
            &&
3835 20
            self::is_binary($str, true) === false
3836
        ) {
3837 2
            return false;
3838
        }
3839
3840 20
        if (self::$SUPPORT['mbstring'] === false) {
3841
            /**
3842
             * @psalm-suppress ImpureFunctionCall - is is only a warning
3843
             */
3844 3
            \trigger_error('UTF8::is_utf32() without mbstring may did not work correctly', \E_USER_WARNING);
3845
        }
3846
3847 20
        $str = self::remove_bom($str);
3848
3849 20
        $maybe_utf32le = 0;
3850 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32LE');
3851 20
        if ($test) {
3852 13
            $test2 = \mb_convert_encoding($test, 'UTF-32LE', 'UTF-8');
3853 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32LE');
3854 13
            if ($test3 === $test) {
3855
                /**
3856
                 * @psalm-suppress RedundantCondition
3857
                 */
3858 13
                if ($str_chars === []) {
3859 13
                    $str_chars = self::count_chars($str, true, false);
3860
                }
3861 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...
3862 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3863 13
                        ++$maybe_utf32le;
3864
                    }
3865
                }
3866 13
                unset($test3charEmpty);
3867
            }
3868
        }
3869
3870 20
        $maybe_utf32be = 0;
3871 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32BE');
3872 20
        if ($test) {
3873 13
            $test2 = \mb_convert_encoding($test, 'UTF-32BE', 'UTF-8');
3874 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32BE');
3875 13
            if ($test3 === $test) {
3876 13
                if ($str_chars === []) {
3877 7
                    $str_chars = self::count_chars($str, true, false);
3878
                }
3879 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...
3880 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3881 13
                        ++$maybe_utf32be;
3882
                    }
3883
                }
3884 13
                unset($test3charEmpty);
3885
            }
3886
        }
3887
3888 20
        if ($maybe_utf32be !== $maybe_utf32le) {
3889 3
            if ($maybe_utf32le > $maybe_utf32be) {
3890 2
                return 1;
3891
            }
3892
3893 3
            return 2;
3894
        }
3895
3896 20
        return false;
3897
    }
3898
3899
    /**
3900
     * Checks whether the passed input contains only byte sequences that appear valid UTF-8.
3901
     *
3902
     * @param int|string|string[]|null $str    <p>The input to be checked.</p>
3903
     * @param bool                     $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
3904
     *
3905
     * @psalm-pure
3906
     *
3907
     * @return bool
3908
     */
3909 82
    public static function is_utf8($str, bool $strict = false): bool
3910
    {
3911 82
        if (\is_array($str) === true) {
3912 2
            foreach ($str as &$v) {
3913 2
                if (self::is_utf8($v, $strict) === false) {
3914 2
                    return false;
3915
                }
3916
            }
3917
3918
            return true;
3919
        }
3920
3921 82
        return self::is_utf8_string((string) $str, $strict);
3922
    }
3923
3924
    /**
3925
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
3926
     * Decodes a JSON string
3927
     *
3928
     * @see http://php.net/manual/en/function.json-decode.php
3929
     *
3930
     * @param string $json    <p>
3931
     *                        The <i>json</i> string being decoded.
3932
     *                        </p>
3933
     *                        <p>
3934
     *                        This function only works with UTF-8 encoded strings.
3935
     *                        </p>
3936
     *                        <p>PHP implements a superset of
3937
     *                        JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
3938
     *                        only supports these values when they are nested inside an array or an object.
3939
     *                        </p>
3940
     * @param bool   $assoc   [optional] <p>
3941
     *                        When <b>TRUE</b>, returned objects will be converted into
3942
     *                        associative arrays.
3943
     *                        </p>
3944
     * @param int    $depth   [optional] <p>
3945
     *                        User specified recursion depth.
3946
     *                        </p>
3947
     * @param int    $options [optional] <p>
3948
     *                        Bitmask of JSON decode options. Currently only
3949
     *                        <b>JSON_BIGINT_AS_STRING</b>
3950
     *                        is supported (default is to cast large integers as floats)
3951
     *                        </p>
3952
     *
3953
     * @psalm-pure
3954
     *
3955
     * @return mixed
3956
     *               The value encoded in <i>json</i> in appropriate PHP type. Values true, false and
3957
     *               null (case-insensitive) are returned as <b>TRUE</b>, <b>FALSE</b> and <b>NULL</b> respectively.
3958
     *               <b>NULL</b> is returned if the <i>json</i> cannot be decoded or if the encoded data
3959
     *               is deeper than the recursion limit.
3960
     */
3961 43
    public static function json_decode(
3962
        string $json,
3963
        bool $assoc = false,
3964
        int $depth = 512,
3965
        int $options = 0
3966
    ) {
3967 43
        $json = self::filter($json);
3968
3969 43
        if (self::$SUPPORT['json'] === false) {
3970
            throw new \RuntimeException('ext-json: is not installed');
3971
        }
3972
3973
        /** @noinspection PhpComposerExtensionStubsInspection */
3974 43
        return \json_decode($json, $assoc, $depth, $options);
3975
    }
3976
3977
    /**
3978
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
3979
     * Returns the JSON representation of a value.
3980
     *
3981
     * @see http://php.net/manual/en/function.json-encode.php
3982
     *
3983
     * @param mixed $value   <p>
3984
     *                       The <i>value</i> being encoded. Can be any type except
3985
     *                       a resource.
3986
     *                       </p>
3987
     *                       <p>
3988
     *                       All string data must be UTF-8 encoded.
3989
     *                       </p>
3990
     *                       <p>PHP implements a superset of
3991
     *                       JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
3992
     *                       only supports these values when they are nested inside an array or an object.
3993
     *                       </p>
3994
     * @param int   $options [optional] <p>
3995
     *                       Bitmask consisting of <b>JSON_HEX_QUOT</b>,
3996
     *                       <b>JSON_HEX_TAG</b>,
3997
     *                       <b>JSON_HEX_AMP</b>,
3998
     *                       <b>JSON_HEX_APOS</b>,
3999
     *                       <b>JSON_NUMERIC_CHECK</b>,
4000
     *                       <b>JSON_PRETTY_PRINT</b>,
4001
     *                       <b>JSON_UNESCAPED_SLASHES</b>,
4002
     *                       <b>JSON_FORCE_OBJECT</b>,
4003
     *                       <b>JSON_UNESCAPED_UNICODE</b>. The behaviour of these
4004
     *                       constants is described on
4005
     *                       the JSON constants page.
4006
     *                       </p>
4007
     * @param int   $depth   [optional] <p>
4008
     *                       Set the maximum depth. Must be greater than zero.
4009
     *                       </p>
4010
     *
4011
     * @psalm-pure
4012
     *
4013
     * @return false|string
4014
     *                      A JSON encoded <strong>string</strong> on success or<br>
4015
     *                      <strong>FALSE</strong> on failure
4016
     */
4017 5
    public static function json_encode($value, int $options = 0, int $depth = 512)
4018
    {
4019 5
        $value = self::filter($value);
4020
4021 5
        if (self::$SUPPORT['json'] === false) {
4022
            throw new \RuntimeException('ext-json: is not installed');
4023
        }
4024
4025
        /** @noinspection PhpComposerExtensionStubsInspection */
4026 5
        return \json_encode($value, $options, $depth);
4027
    }
4028
4029
    /**
4030
     * Checks whether JSON is available on the server.
4031
     *
4032
     * @psalm-pure
4033
     *
4034
     * @return bool
4035
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
4036
     */
4037
    public static function json_loaded(): bool
4038
    {
4039
        return \function_exists('json_decode');
4040
    }
4041
4042
    /**
4043
     * Makes string's first char lowercase.
4044
     *
4045
     * @param string      $str                           <p>The input string</p>
4046
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
4047
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4048
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
4049
     *                                                   tr</p>
4050
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
4051
     *                                                   -> ß</p>
4052
     *
4053
     * @psalm-pure
4054
     *
4055
     * @return string the resulting string
4056
     */
4057 46
    public static function lcfirst(
4058
        string $str,
4059
        string $encoding = 'UTF-8',
4060
        bool $clean_utf8 = false,
4061
        string $lang = null,
4062
        bool $try_to_keep_the_string_length = false
4063
    ): string {
4064 46
        if ($clean_utf8 === true) {
4065
            $str = self::clean($str);
4066
        }
4067
4068 46
        $use_mb_functions = ($lang === null && $try_to_keep_the_string_length === false);
4069
4070 46
        if ($encoding === 'UTF-8') {
4071 43
            $str_part_two = (string) \mb_substr($str, 1);
4072
4073 43
            if ($use_mb_functions === true) {
4074 43
                $str_part_one = \mb_strtolower(
4075 43
                    (string) \mb_substr($str, 0, 1)
4076
                );
4077
            } else {
4078
                $str_part_one = self::strtolower(
4079
                    (string) \mb_substr($str, 0, 1),
4080
                    $encoding,
4081
                    false,
4082
                    $lang,
4083 43
                    $try_to_keep_the_string_length
4084
                );
4085
            }
4086
        } else {
4087 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4088
4089 3
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
4090
4091 3
            $str_part_one = self::strtolower(
4092 3
                (string) self::substr($str, 0, 1, $encoding),
4093 3
                $encoding,
4094 3
                false,
4095 3
                $lang,
4096 3
                $try_to_keep_the_string_length
4097
            );
4098
        }
4099
4100 46
        return $str_part_one . $str_part_two;
4101
    }
4102
4103
    /**
4104
     * alias for "UTF8::lcfirst()"
4105
     *
4106
     * @param string      $str
4107
     * @param string      $encoding
4108
     * @param bool        $clean_utf8
4109
     * @param string|null $lang
4110
     * @param bool        $try_to_keep_the_string_length
4111
     *
4112
     * @psalm-pure
4113
     *
4114
     * @return string
4115
     *
4116
     * @see        UTF8::lcfirst()
4117
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
4118
     */
4119 2
    public static function lcword(
4120
        string $str,
4121
        string $encoding = 'UTF-8',
4122
        bool $clean_utf8 = false,
4123
        string $lang = null,
4124
        bool $try_to_keep_the_string_length = false
4125
    ): string {
4126 2
        return self::lcfirst(
4127 2
            $str,
4128 2
            $encoding,
4129 2
            $clean_utf8,
4130 2
            $lang,
4131 2
            $try_to_keep_the_string_length
4132
        );
4133
    }
4134
4135
    /**
4136
     * Lowercase for all words in the string.
4137
     *
4138
     * @param string      $str                           <p>The input string.</p>
4139
     * @param string[]    $exceptions                    [optional] <p>Exclusion for some words.</p>
4140
     * @param string      $char_list                     [optional] <p>Additional chars that contains to words and do
4141
     *                                                   not start a new word.</p>
4142
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
4143
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4144
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
4145
     *                                                   tr</p>
4146
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
4147
     *                                                   -> ß</p>
4148
     *
4149
     * @psalm-pure
4150
     *
4151
     * @return string
4152
     */
4153 2
    public static function lcwords(
4154
        string $str,
4155
        array $exceptions = [],
4156
        string $char_list = '',
4157
        string $encoding = 'UTF-8',
4158
        bool $clean_utf8 = false,
4159
        string $lang = null,
4160
        bool $try_to_keep_the_string_length = false
4161
    ): string {
4162 2
        if (!$str) {
4163 2
            return '';
4164
        }
4165
4166 2
        $words = self::str_to_words($str, $char_list);
4167 2
        $use_exceptions = $exceptions !== [];
4168
4169 2
        $words_str = '';
4170 2
        foreach ($words as &$word) {
4171 2
            if (!$word) {
4172 2
                continue;
4173
            }
4174
4175
            if (
4176 2
                $use_exceptions === false
4177
                ||
4178 2
                !\in_array($word, $exceptions, true)
4179
            ) {
4180 2
                $words_str .= self::lcfirst($word, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
4181
            } else {
4182 2
                $words_str .= $word;
4183
            }
4184
        }
4185
4186 2
        return $words_str;
4187
    }
4188
4189
    /**
4190
     * alias for "UTF8::lcfirst()"
4191
     *
4192
     * @param string      $str
4193
     * @param string      $encoding
4194
     * @param bool        $clean_utf8
4195
     * @param string|null $lang
4196
     * @param bool        $try_to_keep_the_string_length
4197
     *
4198
     * @psalm-pure
4199
     *
4200
     * @return string
4201
     *
4202
     * @see        UTF8::lcfirst()
4203
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
4204
     */
4205 5
    public static function lowerCaseFirst(
4206
        string $str,
4207
        string $encoding = 'UTF-8',
4208
        bool $clean_utf8 = false,
4209
        string $lang = null,
4210
        bool $try_to_keep_the_string_length = false
4211
    ): string {
4212 5
        return self::lcfirst(
4213 5
            $str,
4214 5
            $encoding,
4215 5
            $clean_utf8,
4216 5
            $lang,
4217 5
            $try_to_keep_the_string_length
4218
        );
4219
    }
4220
4221
    /**
4222
     * Strip whitespace or other characters from the beginning of a UTF-8 string.
4223
     *
4224
     * @param string      $str   <p>The string to be trimmed</p>
4225
     * @param string|null $chars <p>Optional characters to be stripped</p>
4226
     *
4227
     * @psalm-pure
4228
     *
4229
     * @return string the string with unwanted characters stripped from the left
4230
     */
4231 22
    public static function ltrim(string $str = '', string $chars = null): string
4232
    {
4233 22
        if ($str === '') {
4234 3
            return '';
4235
        }
4236
4237 21
        if (self::$SUPPORT['mbstring'] === true) {
4238 21
            if ($chars) {
4239
                /** @noinspection PregQuoteUsageInspection */
4240 10
                $chars = \preg_quote($chars);
4241 10
                $pattern = "^[${chars}]+";
4242
            } else {
4243 14
                $pattern = '^[\\s]+';
4244
            }
4245
4246
            /** @noinspection PhpComposerExtensionStubsInspection */
4247 21
            return (string) \mb_ereg_replace($pattern, '', $str);
4248
        }
4249
4250
        if ($chars) {
4251
            $chars = \preg_quote($chars, '/');
4252
            $pattern = "^[${chars}]+";
4253
        } else {
4254
            $pattern = '^[\\s]+';
4255
        }
4256
4257
        return self::regex_replace($str, $pattern, '', '', '/');
4258
    }
4259
4260
    /**
4261
     * Returns the UTF-8 character with the maximum code point in the given data.
4262
     *
4263
     * @param array<string>|string $arg <p>A UTF-8 encoded string or an array of such strings.</p>
4264
     *
4265
     * @psalm-pure
4266
     *
4267
     * @return string|null the character with the highest code point than others, returns null on failure or empty input
4268
     */
4269 2
    public static function max($arg)
4270
    {
4271 2
        if (\is_array($arg) === true) {
4272 2
            $arg = \implode('', $arg);
4273
        }
4274
4275 2
        $codepoints = self::codepoints($arg, false);
4276 2
        if ($codepoints === []) {
4277 2
            return null;
4278
        }
4279
4280 2
        $codepoint_max = \max($codepoints);
4281
4282 2
        return self::chr($codepoint_max);
4283
    }
4284
4285
    /**
4286
     * Calculates and returns the maximum number of bytes taken by any
4287
     * UTF-8 encoded character in the given string.
4288
     *
4289
     * @param string $str <p>The original Unicode string.</p>
4290
     *
4291
     * @psalm-pure
4292
     *
4293
     * @return int
4294
     *             <p>Max byte lengths of the given chars.</p>
4295
     */
4296 2
    public static function max_chr_width(string $str): int
4297
    {
4298 2
        $bytes = self::chr_size_list($str);
4299 2
        if ($bytes !== []) {
4300 2
            return (int) \max($bytes);
4301
        }
4302
4303 2
        return 0;
4304
    }
4305
4306
    /**
4307
     * Checks whether mbstring is available on the server.
4308
     *
4309
     * @psalm-pure
4310
     *
4311
     * @return bool
4312
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
4313
     */
4314 26
    public static function mbstring_loaded(): bool
4315
    {
4316 26
        return \extension_loaded('mbstring');
4317
    }
4318
4319
    /**
4320
     * Returns the UTF-8 character with the minimum code point in the given data.
4321
     *
4322
     * @param mixed $arg <strong>A UTF-8 encoded string or an array of such strings.</strong>
4323
     *
4324
     * @psalm-pure
4325
     *
4326
     * @return string|null the character with the lowest code point than others, returns null on failure or empty input
4327
     */
4328 2
    public static function min($arg)
4329
    {
4330 2
        if (\is_array($arg) === true) {
4331 2
            $arg = \implode('', $arg);
4332
        }
4333
4334 2
        $codepoints = self::codepoints($arg, false);
4335 2
        if ($codepoints === []) {
4336 2
            return null;
4337
        }
4338
4339 2
        $codepoint_min = \min($codepoints);
4340
4341 2
        return self::chr($codepoint_min);
4342
    }
4343
4344
    /**
4345
     * alias for "UTF8::normalize_encoding()"
4346
     *
4347
     * @param mixed $encoding
4348
     * @param mixed $fallback
4349
     *
4350
     * @psalm-pure
4351
     *
4352
     * @return mixed
4353
     *
4354
     * @see        UTF8::normalize_encoding()
4355
     * @deprecated <p>please use "UTF8::normalize_encoding()"</p>
4356
     */
4357 2
    public static function normalizeEncoding($encoding, $fallback = '')
4358
    {
4359 2
        return self::normalize_encoding($encoding, $fallback);
4360
    }
4361
4362
    /**
4363
     * Normalize the encoding-"name" input.
4364
     *
4365
     * @param mixed $encoding <p>e.g.: ISO, UTF8, WINDOWS-1251 etc.</p>
4366
     * @param mixed $fallback <p>e.g.: UTF-8</p>
4367
     *
4368
     * @psalm-pure
4369
     *
4370
     * @return mixed e.g.: ISO-8859-1, UTF-8, WINDOWS-1251 etc.<br>Will return a empty string as fallback (by default)
4371
     */
4372 331
    public static function normalize_encoding($encoding, $fallback = '')
4373
    {
4374
        /**
4375
         * @psalm-suppress ImpureStaticVariable
4376
         *
4377
         * @var array<string,string>
4378
         */
4379 331
        static $STATIC_NORMALIZE_ENCODING_CACHE = [];
4380
4381
        // init
4382 331
        $encoding = (string) $encoding;
4383
4384 331
        if (!$encoding) {
4385 285
            return $fallback;
4386
        }
4387
4388
        if (
4389 51
            $encoding === 'UTF-8'
4390
            ||
4391 51
            $encoding === 'UTF8'
4392
        ) {
4393 28
            return 'UTF-8';
4394
        }
4395
4396
        if (
4397 43
            $encoding === '8BIT'
4398
            ||
4399 43
            $encoding === 'BINARY'
4400
        ) {
4401
            return 'CP850';
4402
        }
4403
4404
        if (
4405 43
            $encoding === 'HTML'
4406
            ||
4407 43
            $encoding === 'HTML-ENTITIES'
4408
        ) {
4409 2
            return 'HTML-ENTITIES';
4410
        }
4411
4412
        if (
4413 43
            $encoding === 'ISO'
4414
            ||
4415 43
            $encoding === 'ISO-8859-1'
4416
        ) {
4417 39
            return 'ISO-8859-1';
4418
        }
4419
4420
        if (
4421 12
            $encoding === '1' // only a fallback, for non "strict_types" usage ...
4422
            ||
4423 12
            $encoding === '0' // only a fallback, for non "strict_types" usage ...
4424
        ) {
4425 1
            return $fallback;
4426
        }
4427
4428 11
        if (isset($STATIC_NORMALIZE_ENCODING_CACHE[$encoding])) {
4429 8
            return $STATIC_NORMALIZE_ENCODING_CACHE[$encoding];
4430
        }
4431
4432 5
        if (self::$ENCODINGS === null) {
4433 1
            self::$ENCODINGS = self::getData('encodings');
4434
        }
4435
4436 5
        if (\in_array($encoding, self::$ENCODINGS, true)) {
4437 3
            $STATIC_NORMALIZE_ENCODING_CACHE[$encoding] = $encoding;
4438
4439 3
            return $encoding;
4440
        }
4441
4442 4
        $encoding_original = $encoding;
4443 4
        $encoding = \strtoupper($encoding);
4444 4
        $encoding_upper_helper = (string) \preg_replace('/[^a-zA-Z0-9]/u', '', $encoding);
4445
4446
        $equivalences = [
4447 4
            'ISO8859'     => 'ISO-8859-1',
4448
            'ISO88591'    => 'ISO-8859-1',
4449
            'ISO'         => 'ISO-8859-1',
4450
            'LATIN'       => 'ISO-8859-1',
4451
            'LATIN1'      => 'ISO-8859-1', // Western European
4452
            'ISO88592'    => 'ISO-8859-2',
4453
            'LATIN2'      => 'ISO-8859-2', // Central European
4454
            'ISO88593'    => 'ISO-8859-3',
4455
            'LATIN3'      => 'ISO-8859-3', // Southern European
4456
            'ISO88594'    => 'ISO-8859-4',
4457
            'LATIN4'      => 'ISO-8859-4', // Northern European
4458
            'ISO88595'    => 'ISO-8859-5',
4459
            'ISO88596'    => 'ISO-8859-6', // Greek
4460
            'ISO88597'    => 'ISO-8859-7',
4461
            'ISO88598'    => 'ISO-8859-8', // Hebrew
4462
            'ISO88599'    => 'ISO-8859-9',
4463
            'LATIN5'      => 'ISO-8859-9', // Turkish
4464
            'ISO885911'   => 'ISO-8859-11',
4465
            'TIS620'      => 'ISO-8859-11', // Thai
4466
            'ISO885910'   => 'ISO-8859-10',
4467
            'LATIN6'      => 'ISO-8859-10', // Nordic
4468
            'ISO885913'   => 'ISO-8859-13',
4469
            'LATIN7'      => 'ISO-8859-13', // Baltic
4470
            'ISO885914'   => 'ISO-8859-14',
4471
            'LATIN8'      => 'ISO-8859-14', // Celtic
4472
            'ISO885915'   => 'ISO-8859-15',
4473
            'LATIN9'      => 'ISO-8859-15', // Western European (with some extra chars e.g. €)
4474
            'ISO885916'   => 'ISO-8859-16',
4475
            'LATIN10'     => 'ISO-8859-16', // Southeast European
4476
            'CP1250'      => 'WINDOWS-1250',
4477
            'WIN1250'     => 'WINDOWS-1250',
4478
            'WINDOWS1250' => 'WINDOWS-1250',
4479
            'CP1251'      => 'WINDOWS-1251',
4480
            'WIN1251'     => 'WINDOWS-1251',
4481
            'WINDOWS1251' => 'WINDOWS-1251',
4482
            'CP1252'      => 'WINDOWS-1252',
4483
            'WIN1252'     => 'WINDOWS-1252',
4484
            'WINDOWS1252' => 'WINDOWS-1252',
4485
            'CP1253'      => 'WINDOWS-1253',
4486
            'WIN1253'     => 'WINDOWS-1253',
4487
            'WINDOWS1253' => 'WINDOWS-1253',
4488
            'CP1254'      => 'WINDOWS-1254',
4489
            'WIN1254'     => 'WINDOWS-1254',
4490
            'WINDOWS1254' => 'WINDOWS-1254',
4491
            'CP1255'      => 'WINDOWS-1255',
4492
            'WIN1255'     => 'WINDOWS-1255',
4493
            'WINDOWS1255' => 'WINDOWS-1255',
4494
            'CP1256'      => 'WINDOWS-1256',
4495
            'WIN1256'     => 'WINDOWS-1256',
4496
            'WINDOWS1256' => 'WINDOWS-1256',
4497
            'CP1257'      => 'WINDOWS-1257',
4498
            'WIN1257'     => 'WINDOWS-1257',
4499
            'WINDOWS1257' => 'WINDOWS-1257',
4500
            'CP1258'      => 'WINDOWS-1258',
4501
            'WIN1258'     => 'WINDOWS-1258',
4502
            'WINDOWS1258' => 'WINDOWS-1258',
4503
            'UTF16'       => 'UTF-16',
4504
            'UTF32'       => 'UTF-32',
4505
            'UTF8'        => 'UTF-8',
4506
            'UTF'         => 'UTF-8',
4507
            'UTF7'        => 'UTF-7',
4508
            '8BIT'        => 'CP850',
4509
            'BINARY'      => 'CP850',
4510
        ];
4511
4512 4
        if (!empty($equivalences[$encoding_upper_helper])) {
4513 3
            $encoding = $equivalences[$encoding_upper_helper];
4514
        }
4515
4516 4
        $STATIC_NORMALIZE_ENCODING_CACHE[$encoding_original] = $encoding;
4517
4518 4
        return $encoding;
4519
    }
4520
4521
    /**
4522
     * Standardize line ending to unix-like.
4523
     *
4524
     * @param string $str      <p>The input string.</p>
4525
     * @param string $replacer <p>The replacer char e.g. "\n" (Linux) or "\r\n" (Windows). You can also use \PHP_EOL
4526
     *                         here.</p>
4527
     *
4528
     * @psalm-pure
4529
     *
4530
     * @return string
4531
     *                <p>A string with normalized line ending.</p>
4532
     */
4533 5
    public static function normalize_line_ending(string $str, $replacer = "\n"): string
4534
    {
4535 5
        return \str_replace(["\r\n", "\r", "\n"], $replacer, $str);
4536
    }
4537
4538
    /**
4539
     * Normalize some MS Word special characters.
4540
     *
4541
     * @param string $str <p>The string to be normalized.</p>
4542
     *
4543
     * @psalm-pure
4544
     *
4545
     * @return string
4546
     *                <p>A string with normalized characters for commonly used chars in Word documents.</p>
4547
     */
4548 10
    public static function normalize_msword(string $str): string
4549
    {
4550 10
        return ASCII::normalize_msword($str);
4551
    }
4552
4553
    /**
4554
     * Normalize the whitespace.
4555
     *
4556
     * @param string $str                        <p>The string to be normalized.</p>
4557
     * @param bool   $keep_non_breaking_space    [optional] <p>Set to true, to keep non-breaking-spaces.</p>
4558
     * @param bool   $keep_bidi_unicode_controls [optional] <p>Set to true, to keep non-printable (for the web)
4559
     *                                           bidirectional text chars.</p>
4560
     *
4561
     * @psalm-pure
4562
     *
4563
     * @return string
4564
     *                <p>A string with normalized whitespace.</p>
4565
     */
4566 61
    public static function normalize_whitespace(
4567
        string $str,
4568
        bool $keep_non_breaking_space = false,
4569
        bool $keep_bidi_unicode_controls = false
4570
    ): string {
4571 61
        return ASCII::normalize_whitespace(
4572 61
            $str,
4573 61
            $keep_non_breaking_space,
4574 61
            $keep_bidi_unicode_controls
4575
        );
4576
    }
4577
4578
    /**
4579
     * Calculates Unicode code point of the given UTF-8 encoded character.
4580
     *
4581
     * INFO: opposite to UTF8::chr()
4582
     *
4583
     * @param string $chr      <p>The character of which to calculate code point.<p/>
4584
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
4585
     *
4586
     * @psalm-pure
4587
     *
4588
     * @return int
4589
     *             <p>Unicode code point of the given character,<br>
4590
     *             0 on invalid UTF-8 byte sequence</p>
4591
     */
4592 26
    public static function ord($chr, string $encoding = 'UTF-8'): int
4593
    {
4594
        /**
4595
         * @psalm-suppress ImpureStaticVariable
4596
         *
4597
         * @var array<string,int>
4598
         */
4599 26
        static $CHAR_CACHE = [];
4600
4601
        // init
4602 26
        $chr = (string) $chr;
4603
4604 26
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
4605 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4606
        }
4607
4608 26
        $cache_key = $chr . '_' . $encoding;
4609 26
        if (isset($CHAR_CACHE[$cache_key]) === true) {
4610 26
            return $CHAR_CACHE[$cache_key];
4611
        }
4612
4613
        // check again, if it's still not UTF-8
4614 10
        if ($encoding !== 'UTF-8') {
4615 3
            $chr = self::encode($encoding, $chr);
4616
        }
4617
4618 10
        if (self::$ORD === null) {
4619
            self::$ORD = self::getData('ord');
4620
        }
4621
4622 10
        if (isset(self::$ORD[$chr])) {
4623 10
            return $CHAR_CACHE[$cache_key] = self::$ORD[$chr];
4624
        }
4625
4626
        //
4627
        // fallback via "IntlChar"
4628
        //
4629
4630 6
        if (self::$SUPPORT['intlChar'] === true) {
4631
            /** @noinspection PhpComposerExtensionStubsInspection */
4632 5
            $code = \IntlChar::ord($chr);
4633 5
            if ($code) {
4634 5
                return $CHAR_CACHE[$cache_key] = $code;
4635
            }
4636
        }
4637
4638
        //
4639
        // fallback via vanilla php
4640
        //
4641
4642
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
4643 1
        $chr = \unpack('C*', (string) \substr($chr, 0, 4));
4644
        /** @noinspection OffsetOperationsInspection */
4645 1
        $code = $chr ? $chr[1] : 0;
4646
4647
        /** @noinspection OffsetOperationsInspection */
4648 1
        if ($code >= 0xF0 && isset($chr[4])) {
4649
            /** @noinspection UnnecessaryCastingInspection */
4650
            /** @noinspection OffsetOperationsInspection */
4651
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xF0) << 18) + (($chr[2] - 0x80) << 12) + (($chr[3] - 0x80) << 6) + $chr[4] - 0x80);
4652
        }
4653
4654
        /** @noinspection OffsetOperationsInspection */
4655 1
        if ($code >= 0xE0 && isset($chr[3])) {
4656
            /** @noinspection UnnecessaryCastingInspection */
4657
            /** @noinspection OffsetOperationsInspection */
4658 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xE0) << 12) + (($chr[2] - 0x80) << 6) + $chr[3] - 0x80);
4659
        }
4660
4661
        /** @noinspection OffsetOperationsInspection */
4662 1
        if ($code >= 0xC0 && isset($chr[2])) {
4663
            /** @noinspection UnnecessaryCastingInspection */
4664
            /** @noinspection OffsetOperationsInspection */
4665 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xC0) << 6) + $chr[2] - 0x80);
4666
        }
4667
4668
        return $CHAR_CACHE[$cache_key] = $code;
4669
    }
4670
4671
    /**
4672
     * Parses the string into an array (into the the second parameter).
4673
     *
4674
     * WARNING: Unlike "parse_str()", this method does not (re-)place variables in the current scope,
4675
     *          if the second parameter is not set!
4676
     *
4677
     * @see http://php.net/manual/en/function.parse-str.php
4678
     *
4679
     * @param string $str        <p>The input string.</p>
4680
     * @param array  $result     <p>The result will be returned into this reference parameter.</p>
4681
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
4682
     *
4683
     * @psalm-pure
4684
     *
4685
     * @return bool
4686
     *              <p>Will return <strong>false</strong> if php can't parse the string and we haven't any $result.</p>
4687
     */
4688 2
    public static function parse_str(string $str, &$result, bool $clean_utf8 = false): bool
4689
    {
4690 2
        if ($clean_utf8 === true) {
4691 2
            $str = self::clean($str);
4692
        }
4693
4694 2
        if (self::$SUPPORT['mbstring'] === true) {
4695 2
            $return = \mb_parse_str($str, $result);
4696
4697 2
            return $return !== false && $result !== [];
4698
        }
4699
4700
        /**
4701
         * @psalm-suppress ImpureFunctionCall - we use the second parameter, so we don't change variables by magic
4702
         */
4703
        \parse_str($str, $result);
4704
4705
        return $result !== [];
4706
    }
4707
4708
    /**
4709
     * Checks if \u modifier is available that enables Unicode support in PCRE.
4710
     *
4711
     * @psalm-pure
4712
     *
4713
     * @return bool
4714
     *              <p>
4715
     *              <strong>true</strong> if support is available,<br>
4716
     *              <strong>false</strong> otherwise
4717
     *              </p>
4718
     */
4719 102
    public static function pcre_utf8_support(): bool
4720
    {
4721
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
4722 102
        return (bool) @\preg_match('//u', '');
4723
    }
4724
4725
    /**
4726
     * Create an array containing a range of UTF-8 characters.
4727
     *
4728
     * @param mixed     $var1      <p>Numeric or hexadecimal code points, or a UTF-8 character to start from.</p>
4729
     * @param mixed     $var2      <p>Numeric or hexadecimal code points, or a UTF-8 character to end at.</p>
4730
     * @param bool      $use_ctype <p>use ctype to detect numeric and hexadecimal, otherwise we will use a simple
4731
     *                             "is_numeric"</p>
4732
     * @param string    $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
4733
     * @param float|int $step      [optional] <p>
4734
     *                             If a step value is given, it will be used as the
4735
     *                             increment between elements in the sequence. step
4736
     *                             should be given as a positive number. If not specified,
4737
     *                             step will default to 1.
4738
     *                             </p>
4739
     *
4740
     * @psalm-pure
4741
     *
4742
     * @return string[]
4743
     */
4744 2
    public static function range(
4745
        $var1,
4746
        $var2,
4747
        bool $use_ctype = true,
4748
        string $encoding = 'UTF-8',
4749
        $step = 1
4750
    ): array {
4751 2
        if (!$var1 || !$var2) {
4752 2
            return [];
4753
        }
4754
4755 2
        if ($step !== 1) {
4756
            /**
4757
             * @psalm-suppress RedundantConditionGivenDocblockType
4758
             */
4759 1
            if (!\is_numeric($step)) {
0 ignored issues
show
introduced by
The condition is_numeric($step) is always true.
Loading history...
4760
                throw new \InvalidArgumentException('$step need to be a number, type given: ' . \gettype($step));
4761
            }
4762
4763
            /**
4764
             * @psalm-suppress RedundantConditionGivenDocblockType - false-positive from psalm?
4765
             */
4766 1
            if ($step <= 0) {
4767
                throw new \InvalidArgumentException('$step need to be a positive number, given: ' . $step);
4768
            }
4769
        }
4770
4771 2
        if ($use_ctype && self::$SUPPORT['ctype'] === false) {
4772
            throw new \RuntimeException('ext-ctype: is not installed');
4773
        }
4774
4775 2
        $is_digit = false;
4776 2
        $is_xdigit = false;
4777
4778
        /** @noinspection PhpComposerExtensionStubsInspection */
4779 2
        if ($use_ctype && \ctype_digit((string) $var1) && \ctype_digit((string) $var2)) {
4780 2
            $is_digit = true;
4781 2
            $start = (int) $var1;
4782 2
        } /** @noinspection PhpComposerExtensionStubsInspection */ elseif ($use_ctype && \ctype_xdigit($var1) && \ctype_xdigit($var2)) {
4783
            $is_xdigit = true;
4784
            $start = (int) self::hex_to_int($var1);
4785 2
        } elseif (!$use_ctype && \is_numeric($var1)) {
4786 1
            $start = (int) $var1;
4787
        } else {
4788 2
            $start = self::ord($var1);
4789
        }
4790
4791 2
        if (!$start) {
4792
            return [];
4793
        }
4794
4795 2
        if ($is_digit) {
4796 2
            $end = (int) $var2;
4797 2
        } elseif ($is_xdigit) {
4798
            $end = (int) self::hex_to_int($var2);
4799 2
        } elseif (!$use_ctype && \is_numeric($var2)) {
4800 1
            $end = (int) $var2;
4801
        } else {
4802 2
            $end = self::ord($var2);
4803
        }
4804
4805 2
        if (!$end) {
4806
            return [];
4807
        }
4808
4809 2
        $array = [];
4810 2
        foreach (\range($start, $end, $step) as $i) {
4811 2
            $array[] = (string) self::chr((int) $i, $encoding);
4812
        }
4813
4814 2
        return $array;
4815
    }
4816
4817
    /**
4818
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
4819
     *
4820
     * e.g:
4821
     * 'test+test'                     => 'test+test'
4822
     * 'D&#252;sseldorf'               => 'Düsseldorf'
4823
     * 'D%FCsseldorf'                  => 'Düsseldorf'
4824
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
4825
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
4826
     * 'Düsseldorf'                   => 'Düsseldorf'
4827
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
4828
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
4829
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
4830
     *
4831
     * @param string $str          <p>The input string.</p>
4832
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
4833
     *
4834
     * @psalm-pure
4835
     *
4836
     * @return string
4837
     *                <p>The decoded URL, as a string.</p>
4838
     */
4839 7
    public static function rawurldecode(string $str, bool $multi_decode = true): string
4840
    {
4841 7
        if ($str === '') {
4842 4
            return '';
4843
        }
4844
4845
        if (
4846 7
            \strpos($str, '&') === false
4847
            &&
4848 7
            \strpos($str, '%') === false
4849
            &&
4850 7
            \strpos($str, '+') === false
4851
            &&
4852 7
            \strpos($str, '\u') === false
4853
        ) {
4854 4
            return self::fix_simple_utf8($str);
4855
        }
4856
4857 7
        $str = self::urldecode_unicode_helper($str);
4858
4859 7
        if ($multi_decode) {
4860
            do {
4861 6
                $str_compare = $str;
4862
4863
                /**
4864
                 * @psalm-suppress PossiblyInvalidArgument
4865
                 */
4866 6
                $str = self::fix_simple_utf8(
4867 6
                    \rawurldecode(
4868 6
                        self::html_entity_decode(
4869 6
                            self::to_utf8($str),
4870 6
                            \ENT_QUOTES | \ENT_HTML5
4871
                        )
4872
                    )
4873
                );
4874 6
            } while ($str_compare !== $str);
4875
        } else {
4876
            /**
4877
             * @psalm-suppress PossiblyInvalidArgument
4878
             */
4879 1
            $str = self::fix_simple_utf8(
4880 1
                \rawurldecode(
4881 1
                    self::html_entity_decode(
4882 1
                        self::to_utf8($str),
4883 1
                        \ENT_QUOTES | \ENT_HTML5
4884
                    )
4885
                )
4886
            );
4887
        }
4888
4889 7
        return $str;
4890
    }
4891
4892
    /**
4893
     * Replaces all occurrences of $pattern in $str by $replacement.
4894
     *
4895
     * @param string $str         <p>The input string.</p>
4896
     * @param string $pattern     <p>The regular expression pattern.</p>
4897
     * @param string $replacement <p>The string to replace with.</p>
4898
     * @param string $options     [optional] <p>Matching conditions to be used.</p>
4899
     * @param string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
4900
     *
4901
     * @psalm-pure
4902
     *
4903
     * @return string
4904
     */
4905 18
    public static function regex_replace(
4906
        string $str,
4907
        string $pattern,
4908
        string $replacement,
4909
        string $options = '',
4910
        string $delimiter = '/'
4911
    ): string {
4912 18
        if ($options === 'msr') {
4913 9
            $options = 'ms';
4914
        }
4915
4916
        // fallback
4917 18
        if (!$delimiter) {
4918
            $delimiter = '/';
4919
        }
4920
4921 18
        return (string) \preg_replace(
4922 18
            $delimiter . $pattern . $delimiter . 'u' . $options,
4923 18
            $replacement,
4924 18
            $str
4925
        );
4926
    }
4927
4928
    /**
4929
     * alias for "UTF8::remove_bom()"
4930
     *
4931
     * @param string $str
4932
     *
4933
     * @psalm-pure
4934
     *
4935
     * @return string
4936
     *
4937
     * @see        UTF8::remove_bom()
4938
     * @deprecated <p>please use "UTF8::remove_bom()"</p>
4939
     */
4940
    public static function removeBOM(string $str): string
4941
    {
4942
        return self::remove_bom($str);
4943
    }
4944
4945
    /**
4946
     * Remove the BOM from UTF-8 / UTF-16 / UTF-32 strings.
4947
     *
4948
     * @param string $str <p>The input string.</p>
4949
     *
4950
     * @psalm-pure
4951
     *
4952
     * @return string
4953
     *                <p>A string without UTF-BOM.</p>
4954
     */
4955 55
    public static function remove_bom(string $str): string
4956
    {
4957 55
        if ($str === '') {
4958 9
            return '';
4959
        }
4960
4961 55
        $str_length = \strlen($str);
4962 55
        foreach (self::$BOM as $bom_string => $bom_byte_length) {
4963 55
            if (\strpos($str, $bom_string, 0) === 0) {
4964
                /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
4965 11
                $str_tmp = \substr($str, $bom_byte_length, $str_length);
4966 11
                if ($str_tmp === false) {
4967
                    return '';
4968
                }
4969
4970 11
                $str_length -= (int) $bom_byte_length;
4971
4972 55
                $str = (string) $str_tmp;
4973
            }
4974
        }
4975
4976 55
        return $str;
4977
    }
4978
4979
    /**
4980
     * Removes duplicate occurrences of a string in another string.
4981
     *
4982
     * @param string          $str  <p>The base string.</p>
4983
     * @param string|string[] $what <p>String to search for in the base string.</p>
4984
     *
4985
     * @psalm-pure
4986
     *
4987
     * @return string
4988
     *                <p>A string with removed duplicates.</p>
4989
     */
4990 2
    public static function remove_duplicates(string $str, $what = ' '): string
4991
    {
4992 2
        if (\is_string($what) === true) {
4993 2
            $what = [$what];
4994
        }
4995
4996
        /**
4997
         * @psalm-suppress RedundantConditionGivenDocblockType
4998
         */
4999 2
        if (\is_array($what) === true) {
0 ignored issues
show
introduced by
The condition is_array($what) === true is always true.
Loading history...
5000 2
            foreach ($what as $item) {
5001 2
                $str = (string) \preg_replace('/(' . \preg_quote($item, '/') . ')+/u', $item, $str);
5002
            }
5003
        }
5004
5005 2
        return $str;
5006
    }
5007
5008
    /**
5009
     * Remove html via "strip_tags()" from the string.
5010
     *
5011
     * @param string $str            <p>The input string.</p>
5012
     * @param string $allowable_tags [optional] <p>You can use the optional second parameter to specify tags which
5013
     *                               should not be stripped. Default: null
5014
     *                               </p>
5015
     *
5016
     * @psalm-pure
5017
     *
5018
     * @return string
5019
     *                <p>A string with without html tags.</p>
5020
     */
5021 6
    public static function remove_html(string $str, string $allowable_tags = ''): string
5022
    {
5023 6
        return \strip_tags($str, $allowable_tags);
5024
    }
5025
5026
    /**
5027
     * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
5028
     *
5029
     * @param string $str         <p>The input string.</p>
5030
     * @param string $replacement [optional] <p>Default is a empty string.</p>
5031
     *
5032
     * @psalm-pure
5033
     *
5034
     * @return string
5035
     *                <p>A string without breaks.</p>
5036
     */
5037 6
    public static function remove_html_breaks(string $str, string $replacement = ''): string
5038
    {
5039 6
        return (string) \preg_replace("#/\r\n|\r|\n|<br.*/?>#isU", $replacement, $str);
5040
    }
5041
5042
    /**
5043
     * Remove invisible characters from a string.
5044
     *
5045
     * e.g.: This prevents sandwiching null characters between ascii characters, like Java\0script.
5046
     *
5047
     * copy&past from https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Common.php
5048
     *
5049
     * @param string $str         <p>The input string.</p>
5050
     * @param bool   $url_encoded [optional] <p>
5051
     *                            Try to remove url encoded control character.
5052
     *                            WARNING: maybe contains false-positives e.g. aa%0Baa -> aaaa.
5053
     *                            <br>
5054
     *                            Default: false
5055
     *                            </p>
5056
     * @param string $replacement [optional] <p>The replacement character.</p>
5057
     *
5058
     * @psalm-pure
5059
     *
5060
     * @return string
5061
     *                <p>A string without invisible chars.</p>
5062
     */
5063 89
    public static function remove_invisible_characters(
5064
        string $str,
5065
        bool $url_encoded = false,
5066
        string $replacement = ''
5067
    ): string {
5068 89
        return ASCII::remove_invisible_characters(
5069 89
            $str,
5070 89
            $url_encoded,
5071 89
            $replacement
5072
        );
5073
    }
5074
5075
    /**
5076
     * Returns a new string with the prefix $substring removed, if present.
5077
     *
5078
     * @param string $str       <p>The input string.</p>
5079
     * @param string $substring <p>The prefix to remove.</p>
5080
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5081
     *
5082
     * @psalm-pure
5083
     *
5084
     * @return string
5085
     *                <p>A string without the prefix $substring.</p>
5086
     */
5087 12
    public static function remove_left(
5088
        string $str,
5089
        string $substring,
5090
        string $encoding = 'UTF-8'
5091
    ): string {
5092 12
        if ($substring && \strpos($str, $substring) === 0) {
5093 6
            if ($encoding === 'UTF-8') {
5094 4
                return (string) \mb_substr(
5095 4
                    $str,
5096 4
                    (int) \mb_strlen($substring)
5097
                );
5098
            }
5099
5100 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5101
5102 2
            return (string) self::substr(
5103 2
                $str,
5104 2
                (int) self::strlen($substring, $encoding),
5105 2
                null,
5106 2
                $encoding
5107
            );
5108
        }
5109
5110 6
        return $str;
5111
    }
5112
5113
    /**
5114
     * Returns a new string with the suffix $substring removed, if present.
5115
     *
5116
     * @param string $str
5117
     * @param string $substring <p>The suffix to remove.</p>
5118
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5119
     *
5120
     * @psalm-pure
5121
     *
5122
     * @return string
5123
     *                <p>A string having a $str without the suffix $substring.</p>
5124
     */
5125 12
    public static function remove_right(
5126
        string $str,
5127
        string $substring,
5128
        string $encoding = 'UTF-8'
5129
    ): string {
5130 12
        if ($substring && \substr($str, -\strlen($substring)) === $substring) {
5131 6
            if ($encoding === 'UTF-8') {
5132 4
                return (string) \mb_substr(
5133 4
                    $str,
5134 4
                    0,
5135 4
                    (int) \mb_strlen($str) - (int) \mb_strlen($substring)
5136
                );
5137
            }
5138
5139 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5140
5141 2
            return (string) self::substr(
5142 2
                $str,
5143 2
                0,
5144 2
                (int) self::strlen($str, $encoding) - (int) self::strlen($substring, $encoding),
5145 2
                $encoding
5146
            );
5147
        }
5148
5149 6
        return $str;
5150
    }
5151
5152
    /**
5153
     * Replaces all occurrences of $search in $str by $replacement.
5154
     *
5155
     * @param string $str            <p>The input string.</p>
5156
     * @param string $search         <p>The needle to search for.</p>
5157
     * @param string $replacement    <p>The string to replace with.</p>
5158
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5159
     *
5160
     * @psalm-pure
5161
     *
5162
     * @return string
5163
     *                <p>A string with replaced parts.</p>
5164
     */
5165 29
    public static function replace(
5166
        string $str,
5167
        string $search,
5168
        string $replacement,
5169
        bool $case_sensitive = true
5170
    ): string {
5171 29
        if ($case_sensitive) {
5172 22
            return \str_replace($search, $replacement, $str);
5173
        }
5174
5175 7
        return self::str_ireplace($search, $replacement, $str);
5176
    }
5177
5178
    /**
5179
     * Replaces all occurrences of $search in $str by $replacement.
5180
     *
5181
     * @param string       $str            <p>The input string.</p>
5182
     * @param array        $search         <p>The elements to search for.</p>
5183
     * @param array|string $replacement    <p>The string to replace with.</p>
5184
     * @param bool         $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5185
     *
5186
     * @psalm-pure
5187
     *
5188
     * @return string
5189
     *                <p>A string with replaced parts.</p>
5190
     */
5191 30
    public static function replace_all(
5192
        string $str,
5193
        array $search,
5194
        $replacement,
5195
        bool $case_sensitive = true
5196
    ): string {
5197 30
        if ($case_sensitive) {
5198 23
            return \str_replace($search, $replacement, $str);
5199
        }
5200
5201 7
        return self::str_ireplace($search, $replacement, $str);
5202
    }
5203
5204
    /**
5205
     * Replace the diamond question mark (�) and invalid-UTF8 chars with the replacement.
5206
     *
5207
     * @param string $str                        <p>The input string</p>
5208
     * @param string $replacement_char           <p>The replacement character.</p>
5209
     * @param bool   $process_invalid_utf8_chars <p>Convert invalid UTF-8 chars </p>
5210
     *
5211
     * @psalm-pure
5212
     *
5213
     * @return string
5214
     *                <p>A string without diamond question marks (�).</p>
5215
     */
5216 35
    public static function replace_diamond_question_mark(
5217
        string $str,
5218
        string $replacement_char = '',
5219
        bool $process_invalid_utf8_chars = true
5220
    ): string {
5221 35
        if ($str === '') {
5222 9
            return '';
5223
        }
5224
5225 35
        if ($process_invalid_utf8_chars === true) {
5226 35
            $replacement_char_helper = $replacement_char;
5227 35
            if ($replacement_char === '') {
5228 35
                $replacement_char_helper = 'none';
5229
            }
5230
5231 35
            if (self::$SUPPORT['mbstring'] === false) {
5232
                // if there is no native support for "mbstring",
5233
                // then we need to clean the string before ...
5234
                $str = self::clean($str);
5235
            }
5236
5237
            /**
5238
             * @psalm-suppress ImpureFunctionCall - we will reset the value in the next step
5239
             */
5240 35
            $save = \mb_substitute_character();
5241 35
            \mb_substitute_character($replacement_char_helper);
5242
            // the polyfill maybe return false, so cast to string
5243 35
            $str = (string) \mb_convert_encoding($str, 'UTF-8', 'UTF-8');
5244 35
            \mb_substitute_character($save);
5245
        }
5246
5247 35
        return \str_replace(
5248
            [
5249 35
                "\xEF\xBF\xBD",
5250
                '�',
5251
            ],
5252
            [
5253 35
                $replacement_char,
5254 35
                $replacement_char,
5255
            ],
5256 35
            $str
5257
        );
5258
    }
5259
5260
    /**
5261
     * Strip whitespace or other characters from the end of a UTF-8 string.
5262
     *
5263
     * @param string      $str   <p>The string to be trimmed.</p>
5264
     * @param string|null $chars <p>Optional characters to be stripped.</p>
5265
     *
5266
     * @psalm-pure
5267
     *
5268
     * @return string
5269
     *                <p>A string with unwanted characters stripped from the right.</p>
5270
     */
5271 20
    public static function rtrim(string $str = '', string $chars = null): string
5272
    {
5273 20
        if ($str === '') {
5274 3
            return '';
5275
        }
5276
5277 19
        if (self::$SUPPORT['mbstring'] === true) {
5278 19
            if ($chars) {
5279
                /** @noinspection PregQuoteUsageInspection */
5280 8
                $chars = \preg_quote($chars);
5281 8
                $pattern = "[${chars}]+$";
5282
            } else {
5283 14
                $pattern = '[\\s]+$';
5284
            }
5285
5286
            /** @noinspection PhpComposerExtensionStubsInspection */
5287 19
            return (string) \mb_ereg_replace($pattern, '', $str);
5288
        }
5289
5290
        if ($chars) {
5291
            $chars = \preg_quote($chars, '/');
5292
            $pattern = "[${chars}]+$";
5293
        } else {
5294
            $pattern = '[\\s]+$';
5295
        }
5296
5297
        return self::regex_replace($str, $pattern, '', '', '/');
5298
    }
5299
5300
    /**
5301
     * WARNING: Print native UTF-8 support (libs) by default, e.g. for debugging.
5302
     *
5303
     * @param bool $useEcho
5304
     *
5305
     * @psalm-pure
5306
     *
5307
     * @return string|void
5308
     */
5309 2
    public static function showSupport(bool $useEcho = true)
5310
    {
5311
        // init
5312 2
        $html = '';
5313
5314 2
        $html .= '<pre>';
5315
        /** @noinspection AlterInForeachInspection */
5316 2
        foreach (self::$SUPPORT as $key => &$value) {
5317 2
            $html .= $key . ' - ' . \print_r($value, true) . "\n<br>";
5318
        }
5319 2
        $html .= '</pre>';
5320
5321 2
        if ($useEcho) {
5322 2
            echo $html;
5323
        }
5324
5325 2
        return $html;
5326
    }
5327
5328
    /**
5329
     * Converts a UTF-8 character to HTML Numbered Entity like "&#123;".
5330
     *
5331
     * @param string $char             <p>The Unicode character to be encoded as numbered entity.</p>
5332
     * @param bool   $keep_ascii_chars <p>Set to <strong>true</strong> to keep ASCII chars.</>
5333
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
5334
     *
5335
     * @psalm-pure
5336
     *
5337
     * @return string
5338
     *                <p>The HTML numbered entity for the given character.</p>
5339
     */
5340 2
    public static function single_chr_html_encode(
5341
        string $char,
5342
        bool $keep_ascii_chars = false,
5343
        string $encoding = 'UTF-8'
5344
    ): string {
5345 2
        if ($char === '') {
5346 2
            return '';
5347
        }
5348
5349
        if (
5350 2
            $keep_ascii_chars === true
5351
            &&
5352 2
            ASCII::is_ascii($char) === true
5353
        ) {
5354 2
            return $char;
5355
        }
5356
5357 2
        return '&#' . self::ord($char, $encoding) . ';';
5358
    }
5359
5360
    /**
5361
     * @param string $str
5362
     * @param int    $tab_length
5363
     *
5364
     * @psalm-pure
5365
     *
5366
     * @return string
5367
     */
5368 5
    public static function spaces_to_tabs(string $str, int $tab_length = 4): string
5369
    {
5370 5
        if ($tab_length === 4) {
5371 3
            $tab = '    ';
5372 2
        } elseif ($tab_length === 2) {
5373 1
            $tab = '  ';
5374
        } else {
5375 1
            $tab = \str_repeat(' ', $tab_length);
5376
        }
5377
5378 5
        return \str_replace($tab, "\t", $str);
5379
    }
5380
5381
    /**
5382
     * alias for "UTF8::str_split()"
5383
     *
5384
     * @param string|string[] $str
5385
     * @param int             $length
5386
     * @param bool            $clean_utf8
5387
     *
5388
     * @psalm-pure
5389
     *
5390
     * @return string[]
5391
     *
5392
     * @see        UTF8::str_split()
5393
     * @deprecated <p>please use "UTF8::str_split()"</p>
5394
     */
5395 9
    public static function split(
5396
        $str,
5397
        int $length = 1,
5398
        bool $clean_utf8 = false
5399
    ): array {
5400 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 which are incompatible with the documented value type string.
Loading history...
5401
    }
5402
5403
    /**
5404
     * alias for "UTF8::str_starts_with()"
5405
     *
5406
     * @param string $haystack
5407
     * @param string $needle
5408
     *
5409
     * @psalm-pure
5410
     *
5411
     * @return bool
5412
     *
5413
     * @see        UTF8::str_starts_with()
5414
     * @deprecated <p>please use "UTF8::str_starts_with()"</p>
5415
     */
5416
    public static function str_begins(string $haystack, string $needle): bool
5417
    {
5418
        return self::str_starts_with($haystack, $needle);
5419
    }
5420
5421
    /**
5422
     * Returns a camelCase version of the string. Trims surrounding spaces,
5423
     * capitalizes letters following digits, spaces, dashes and underscores,
5424
     * and removes spaces, dashes, as well as underscores.
5425
     *
5426
     * @param string      $str                           <p>The input string.</p>
5427
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
5428
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5429
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5430
     *                                                   tr</p>
5431
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
5432
     *                                                   -> ß</p>
5433
     *
5434
     * @psalm-pure
5435
     *
5436
     * @return string
5437
     */
5438 32
    public static function str_camelize(
5439
        string $str,
5440
        string $encoding = 'UTF-8',
5441
        bool $clean_utf8 = false,
5442
        string $lang = null,
5443
        bool $try_to_keep_the_string_length = false
5444
    ): string {
5445 32
        if ($clean_utf8 === true) {
5446
            $str = self::clean($str);
5447
        }
5448
5449 32
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
5450 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5451
        }
5452
5453 32
        $str = self::lcfirst(
5454 32
            \trim($str),
5455 32
            $encoding,
5456 32
            false,
5457 32
            $lang,
5458 32
            $try_to_keep_the_string_length
5459
        );
5460 32
        $str = (string) \preg_replace('/^[-_]+/', '', $str);
5461
5462 32
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5463
5464 32
        $str = (string) \preg_replace_callback(
5465 32
            '/[-_\\s]+(.)?/u',
5466
            /**
5467
             * @param array $match
5468
             *
5469
             * @psalm-pure
5470
             *
5471
             * @return string
5472
             */
5473
            static function (array $match) use ($use_mb_functions, $encoding, $lang, $try_to_keep_the_string_length): string {
5474 27
                if (isset($match[1])) {
5475 27
                    if ($use_mb_functions === true) {
5476 27
                        if ($encoding === 'UTF-8') {
5477 27
                            return \mb_strtoupper($match[1]);
5478
                        }
5479
5480
                        return \mb_strtoupper($match[1], $encoding);
5481
                    }
5482
5483
                    return self::strtoupper($match[1], $encoding, false, $lang, $try_to_keep_the_string_length);
5484
                }
5485
5486 1
                return '';
5487 32
            },
5488 32
            $str
5489
        );
5490
5491 32
        return (string) \preg_replace_callback(
5492 32
            '/[\\p{N}]+(.)?/u',
5493
            /**
5494
             * @param array $match
5495
             *
5496
             * @psalm-pure
5497
             *
5498
             * @return string
5499
             */
5500
            static function (array $match) use ($use_mb_functions, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length): string {
5501 6
                if ($use_mb_functions === true) {
5502 6
                    if ($encoding === 'UTF-8') {
5503 6
                        return \mb_strtoupper($match[0]);
5504
                    }
5505
5506
                    return \mb_strtoupper($match[0], $encoding);
5507
                }
5508
5509
                return self::strtoupper($match[0], $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5510 32
            },
5511 32
            $str
5512
        );
5513
    }
5514
5515
    /**
5516
     * Returns the string with the first letter of each word capitalized,
5517
     * except for when the word is a name which shouldn't be capitalized.
5518
     *
5519
     * @param string $str
5520
     *
5521
     * @psalm-pure
5522
     *
5523
     * @return string
5524
     *                <p>A string with $str capitalized.</p>
5525
     */
5526 1
    public static function str_capitalize_name(string $str): string
5527
    {
5528 1
        return self::str_capitalize_name_helper(
5529 1
            self::str_capitalize_name_helper(
5530 1
                self::collapse_whitespace($str),
5531 1
                ' '
5532
            ),
5533 1
            '-'
5534
        );
5535
    }
5536
5537
    /**
5538
     * Returns true if the string contains $needle, false otherwise. By default
5539
     * the comparison is case-sensitive, but can be made insensitive by setting
5540
     * $case_sensitive to false.
5541
     *
5542
     * @param string $haystack       <p>The input string.</p>
5543
     * @param string $needle         <p>Substring to look for.</p>
5544
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5545
     *
5546
     * @psalm-pure
5547
     *
5548
     * @return bool whether or not $haystack contains $needle
5549
     */
5550 21
    public static function str_contains(
5551
        string $haystack,
5552
        string $needle,
5553
        bool $case_sensitive = true
5554
    ): bool {
5555 21
        if ($case_sensitive) {
5556 11
            return \strpos($haystack, $needle) !== false;
5557
        }
5558
5559 10
        return \mb_stripos($haystack, $needle) !== false;
5560
    }
5561
5562
    /**
5563
     * Returns true if the string contains all $needles, false otherwise. By
5564
     * default the comparison is case-sensitive, but can be made insensitive by
5565
     * setting $case_sensitive to false.
5566
     *
5567
     * @param string $haystack       <p>The input string.</p>
5568
     * @param array  $needles        <p>SubStrings to look for.</p>
5569
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5570
     *
5571
     * @psalm-pure
5572
     *
5573
     * @return bool whether or not $haystack contains $needle
5574
     */
5575 45
    public static function str_contains_all(
5576
        string $haystack,
5577
        array $needles,
5578
        bool $case_sensitive = true
5579
    ): bool {
5580 45
        if ($haystack === '' || $needles === []) {
5581 1
            return false;
5582
        }
5583
5584
        /** @noinspection LoopWhichDoesNotLoopInspection */
5585 44
        foreach ($needles as &$needle) {
5586 44
            if ($case_sensitive) {
5587
                /** @noinspection NestedPositiveIfStatementsInspection */
5588 24
                if (!$needle || \strpos($haystack, $needle) === false) {
5589 12
                    return false;
5590
                }
5591
            }
5592
5593 33
            if (!$needle || \mb_stripos($haystack, $needle) === false) {
5594 33
                return false;
5595
            }
5596
        }
5597
5598 24
        return true;
5599
    }
5600
5601
    /**
5602
     * Returns true if the string contains any $needles, false otherwise. By
5603
     * default the comparison is case-sensitive, but can be made insensitive by
5604
     * setting $case_sensitive to false.
5605
     *
5606
     * @param string $haystack       <p>The input string.</p>
5607
     * @param array  $needles        <p>SubStrings to look for.</p>
5608
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5609
     *
5610
     * @psalm-pure
5611
     *
5612
     * @return bool
5613
     *              Whether or not $str contains $needle
5614
     */
5615 46
    public static function str_contains_any(
5616
        string $haystack,
5617
        array $needles,
5618
        bool $case_sensitive = true
5619
    ): bool {
5620 46
        if ($haystack === '' || $needles === []) {
5621 1
            return false;
5622
        }
5623
5624
        /** @noinspection LoopWhichDoesNotLoopInspection */
5625 45
        foreach ($needles as &$needle) {
5626 45
            if (!$needle) {
5627
                continue;
5628
            }
5629
5630 45
            if ($case_sensitive) {
5631 25
                if (\strpos($haystack, $needle) !== false) {
5632 14
                    return true;
5633
                }
5634
5635 13
                continue;
5636
            }
5637
5638 20
            if (\mb_stripos($haystack, $needle) !== false) {
5639 20
                return true;
5640
            }
5641
        }
5642
5643 19
        return false;
5644
    }
5645
5646
    /**
5647
     * Returns a lowercase and trimmed string separated by dashes. Dashes are
5648
     * inserted before uppercase characters (with the exception of the first
5649
     * character of the string), and in place of spaces as well as underscores.
5650
     *
5651
     * @param string $str      <p>The input string.</p>
5652
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5653
     *
5654
     * @psalm-pure
5655
     *
5656
     * @return string
5657
     */
5658 19
    public static function str_dasherize(string $str, string $encoding = 'UTF-8'): string
5659
    {
5660 19
        return self::str_delimit($str, '-', $encoding);
5661
    }
5662
5663
    /**
5664
     * Returns a lowercase and trimmed string separated by the given delimiter.
5665
     * Delimiters are inserted before uppercase characters (with the exception
5666
     * of the first character of the string), and in place of spaces, dashes,
5667
     * and underscores. Alpha delimiters are not converted to lowercase.
5668
     *
5669
     * @param string      $str                           <p>The input string.</p>
5670
     * @param string      $delimiter                     <p>Sequence used to separate parts of the string.</p>
5671
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
5672
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5673
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5674
     *                                                   tr</p>
5675
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
5676
     *                                                   ß</p>
5677
     *
5678
     * @psalm-pure
5679
     *
5680
     * @return string
5681
     */
5682 49
    public static function str_delimit(
5683
        string $str,
5684
        string $delimiter,
5685
        string $encoding = 'UTF-8',
5686
        bool $clean_utf8 = false,
5687
        string $lang = null,
5688
        bool $try_to_keep_the_string_length = false
5689
    ): string {
5690 49
        if (self::$SUPPORT['mbstring'] === true) {
5691
            /** @noinspection PhpComposerExtensionStubsInspection */
5692 49
            $str = (string) \mb_ereg_replace('\\B(\\p{Lu})', '-\1', \trim($str));
5693
5694 49
            $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5695 49
            if ($use_mb_functions === true && $encoding === 'UTF-8') {
5696 22
                $str = \mb_strtolower($str);
5697
            } else {
5698 27
                $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5699
            }
5700
5701
            /** @noinspection PhpComposerExtensionStubsInspection */
5702 49
            return (string) \mb_ereg_replace('[\\-_\\s]+', $delimiter, $str);
5703
        }
5704
5705
        $str = (string) \preg_replace('/\\B(\\p{Lu})/u', '-\1', \trim($str));
5706
5707
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5708
        if ($use_mb_functions === true && $encoding === 'UTF-8') {
5709
            $str = \mb_strtolower($str);
5710
        } else {
5711
            $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5712
        }
5713
5714
        return (string) \preg_replace('/[\\-_\\s]+/u', $delimiter, $str);
5715
    }
5716
5717
    /**
5718
     * Optimized "mb_detect_encoding()"-function -> with support for UTF-16 and UTF-32.
5719
     *
5720
     * @param string $str <p>The input string.</p>
5721
     *
5722
     * @psalm-pure
5723
     *
5724
     * @return false|string
5725
     *                      The detected string-encoding e.g. UTF-8 or UTF-16BE,<br>
5726
     *                      otherwise it will return false e.g. for BINARY or not detected encoding.
5727
     */
5728 30
    public static function str_detect_encoding($str)
5729
    {
5730
        // init
5731 30
        $str = (string) $str;
5732
5733
        //
5734
        // 1.) check binary strings (010001001...) like UTF-16 / UTF-32 / PDF / Images / ...
5735
        //
5736
5737 30
        if (self::is_binary($str, true) === true) {
5738 11
            $is_utf32 = self::is_utf32($str, false);
5739 11
            if ($is_utf32 === 1) {
5740
                return 'UTF-32LE';
5741
            }
5742 11
            if ($is_utf32 === 2) {
5743 1
                return 'UTF-32BE';
5744
            }
5745
5746 11
            $is_utf16 = self::is_utf16($str, false);
5747 11
            if ($is_utf16 === 1) {
5748 3
                return 'UTF-16LE';
5749
            }
5750 11
            if ($is_utf16 === 2) {
5751 2
                return 'UTF-16BE';
5752
            }
5753
5754
            // is binary but not "UTF-16" or "UTF-32"
5755 9
            return false;
5756
        }
5757
5758
        //
5759
        // 2.) simple check for ASCII chars
5760
        //
5761
5762 26
        if (ASCII::is_ascii($str) === true) {
5763 10
            return 'ASCII';
5764
        }
5765
5766
        //
5767
        // 3.) simple check for UTF-8 chars
5768
        //
5769
5770 26
        if (self::is_utf8_string($str) === true) {
5771 19
            return 'UTF-8';
5772
        }
5773
5774
        //
5775
        // 4.) check via "mb_detect_encoding()"
5776
        //
5777
        // INFO: UTF-16, UTF-32, UCS2 and UCS4, encoding detection will fail always with "mb_detect_encoding()"
5778
5779
        $encoding_detecting_order = [
5780 15
            'ISO-8859-1',
5781
            'ISO-8859-2',
5782
            'ISO-8859-3',
5783
            'ISO-8859-4',
5784
            'ISO-8859-5',
5785
            'ISO-8859-6',
5786
            'ISO-8859-7',
5787
            'ISO-8859-8',
5788
            'ISO-8859-9',
5789
            'ISO-8859-10',
5790
            'ISO-8859-13',
5791
            'ISO-8859-14',
5792
            'ISO-8859-15',
5793
            'ISO-8859-16',
5794
            'WINDOWS-1251',
5795
            'WINDOWS-1252',
5796
            'WINDOWS-1254',
5797
            'CP932',
5798
            'CP936',
5799
            'CP950',
5800
            'CP866',
5801
            'CP850',
5802
            'CP51932',
5803
            'CP50220',
5804
            'CP50221',
5805
            'CP50222',
5806
            'ISO-2022-JP',
5807
            'ISO-2022-KR',
5808
            'JIS',
5809
            'JIS-ms',
5810
            'EUC-CN',
5811
            'EUC-JP',
5812
        ];
5813
5814 15
        if (self::$SUPPORT['mbstring'] === true) {
5815
            // info: do not use the symfony polyfill here
5816 15
            $encoding = \mb_detect_encoding($str, $encoding_detecting_order, true);
5817 15
            if ($encoding) {
5818 15
                return $encoding;
5819
            }
5820
        }
5821
5822
        //
5823
        // 5.) check via "iconv()"
5824
        //
5825
5826
        if (self::$ENCODINGS === null) {
5827
            self::$ENCODINGS = self::getData('encodings');
5828
        }
5829
5830
        foreach (self::$ENCODINGS as $encoding_tmp) {
5831
            // INFO: //IGNORE but still throw notice
5832
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
5833
            if ((string) @\iconv($encoding_tmp, $encoding_tmp . '//IGNORE', $str) === $str) {
5834
                return $encoding_tmp;
5835
            }
5836
        }
5837
5838
        return false;
5839
    }
5840
5841
    /**
5842
     * alias for "UTF8::str_ends_with()"
5843
     *
5844
     * @param string $haystack
5845
     * @param string $needle
5846
     *
5847
     * @psalm-pure
5848
     *
5849
     * @return bool
5850
     *
5851
     * @see        UTF8::str_ends_with()
5852
     * @deprecated <p>please use "UTF8::str_ends_with()"</p>
5853
     */
5854
    public static function str_ends(string $haystack, string $needle): bool
5855
    {
5856
        return self::str_ends_with($haystack, $needle);
5857
    }
5858
5859
    /**
5860
     * Check if the string ends with the given substring.
5861
     *
5862
     * @param string $haystack <p>The string to search in.</p>
5863
     * @param string $needle   <p>The substring to search for.</p>
5864
     *
5865
     * @psalm-pure
5866
     *
5867
     * @return bool
5868
     */
5869 9
    public static function str_ends_with(string $haystack, string $needle): bool
5870
    {
5871 9
        if ($needle === '') {
5872 2
            return true;
5873
        }
5874
5875 9
        if ($haystack === '') {
5876
            return false;
5877
        }
5878
5879 9
        return \substr($haystack, -\strlen($needle)) === $needle;
5880
    }
5881
5882
    /**
5883
     * Returns true if the string ends with any of $substrings, false otherwise.
5884
     *
5885
     * - case-sensitive
5886
     *
5887
     * @param string   $str        <p>The input string.</p>
5888
     * @param string[] $substrings <p>Substrings to look for.</p>
5889
     *
5890
     * @psalm-pure
5891
     *
5892
     * @return bool whether or not $str ends with $substring
5893
     */
5894 7
    public static function str_ends_with_any(string $str, array $substrings): bool
5895
    {
5896 7
        if ($substrings === []) {
5897
            return false;
5898
        }
5899
5900 7
        foreach ($substrings as &$substring) {
5901 7
            if (\substr($str, -\strlen($substring)) === $substring) {
5902 7
                return true;
5903
            }
5904
        }
5905
5906 6
        return false;
5907
    }
5908
5909
    /**
5910
     * Ensures that the string begins with $substring. If it doesn't, it's
5911
     * prepended.
5912
     *
5913
     * @param string $str       <p>The input string.</p>
5914
     * @param string $substring <p>The substring to add if not present.</p>
5915
     *
5916
     * @psalm-pure
5917
     *
5918
     * @return string
5919
     */
5920 10
    public static function str_ensure_left(string $str, string $substring): string
5921
    {
5922
        if (
5923 10
            $substring !== ''
5924
            &&
5925 10
            \strpos($str, $substring) === 0
5926
        ) {
5927 6
            return $str;
5928
        }
5929
5930 4
        return $substring . $str;
5931
    }
5932
5933
    /**
5934
     * Ensures that the string ends with $substring. If it doesn't, it's appended.
5935
     *
5936
     * @param string $str       <p>The input string.</p>
5937
     * @param string $substring <p>The substring to add if not present.</p>
5938
     *
5939
     * @psalm-pure
5940
     *
5941
     * @return string
5942
     */
5943 10
    public static function str_ensure_right(string $str, string $substring): string
5944
    {
5945
        if (
5946 10
            $str === ''
5947
            ||
5948 10
            $substring === ''
5949
            ||
5950 10
            \substr($str, -\strlen($substring)) !== $substring
5951
        ) {
5952 4
            $str .= $substring;
5953
        }
5954
5955 10
        return $str;
5956
    }
5957
5958
    /**
5959
     * Capitalizes the first word of the string, replaces underscores with
5960
     * spaces, and strips '_id'.
5961
     *
5962
     * @param string $str
5963
     *
5964
     * @psalm-pure
5965
     *
5966
     * @return string
5967
     */
5968 3
    public static function str_humanize($str): string
5969
    {
5970 3
        $str = \str_replace(
5971
            [
5972 3
                '_id',
5973
                '_',
5974
            ],
5975
            [
5976 3
                '',
5977
                ' ',
5978
            ],
5979 3
            $str
5980
        );
5981
5982 3
        return self::ucfirst(\trim($str));
5983
    }
5984
5985
    /**
5986
     * alias for "UTF8::str_istarts_with()"
5987
     *
5988
     * @param string $haystack
5989
     * @param string $needle
5990
     *
5991
     * @psalm-pure
5992
     *
5993
     * @return bool
5994
     *
5995
     * @see        UTF8::str_istarts_with()
5996
     * @deprecated <p>please use "UTF8::str_istarts_with()"</p>
5997
     */
5998
    public static function str_ibegins(string $haystack, string $needle): bool
5999
    {
6000
        return self::str_istarts_with($haystack, $needle);
6001
    }
6002
6003
    /**
6004
     * alias for "UTF8::str_iends_with()"
6005
     *
6006
     * @param string $haystack
6007
     * @param string $needle
6008
     *
6009
     * @psalm-pure
6010
     *
6011
     * @return bool
6012
     *
6013
     * @see        UTF8::str_iends_with()
6014
     * @deprecated <p>please use "UTF8::str_iends_with()"</p>
6015
     */
6016
    public static function str_iends(string $haystack, string $needle): bool
6017
    {
6018
        return self::str_iends_with($haystack, $needle);
6019
    }
6020
6021
    /**
6022
     * Check if the string ends with the given substring, case-insensitive.
6023
     *
6024
     * @param string $haystack <p>The string to search in.</p>
6025
     * @param string $needle   <p>The substring to search for.</p>
6026
     *
6027
     * @psalm-pure
6028
     *
6029
     * @return bool
6030
     */
6031 12
    public static function str_iends_with(string $haystack, string $needle): bool
6032
    {
6033 12
        if ($needle === '') {
6034 2
            return true;
6035
        }
6036
6037 12
        if ($haystack === '') {
6038
            return false;
6039
        }
6040
6041 12
        return self::strcasecmp(\substr($haystack, -\strlen($needle)), $needle) === 0;
6042
    }
6043
6044
    /**
6045
     * Returns true if the string ends with any of $substrings, false otherwise.
6046
     *
6047
     * - case-insensitive
6048
     *
6049
     * @param string   $str        <p>The input string.</p>
6050
     * @param string[] $substrings <p>Substrings to look for.</p>
6051
     *
6052
     * @psalm-pure
6053
     *
6054
     * @return bool
6055
     *              <p>Whether or not $str ends with $substring.</p>
6056
     */
6057 4
    public static function str_iends_with_any(string $str, array $substrings): bool
6058
    {
6059 4
        if ($substrings === []) {
6060
            return false;
6061
        }
6062
6063 4
        foreach ($substrings as &$substring) {
6064 4
            if (self::str_iends_with($str, $substring)) {
6065 4
                return true;
6066
            }
6067
        }
6068
6069
        return false;
6070
    }
6071
6072
    /**
6073
     * Returns the index of the first occurrence of $needle in the string,
6074
     * and false if not found. Accepts an optional offset from which to begin
6075
     * the search.
6076
     *
6077
     * @param string $str      <p>The input string.</p>
6078
     * @param string $needle   <p>Substring to look for.</p>
6079
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6080
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6081
     *
6082
     * @psalm-pure
6083
     *
6084
     * @return false|int
6085
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6086
     *
6087
     * @see        UTF8::stripos()
6088
     * @deprecated <p>please use "UTF8::stripos()"</p>
6089
     */
6090
    public static function str_iindex_first(
6091
        string $str,
6092
        string $needle,
6093
        int $offset = 0,
6094
        string $encoding = 'UTF-8'
6095
    ) {
6096
        return self::stripos(
6097
            $str,
6098
            $needle,
6099
            $offset,
6100
            $encoding
6101
        );
6102
    }
6103
6104
    /**
6105
     * Returns the index of the last occurrence of $needle in the string,
6106
     * and false if not found. Accepts an optional offset from which to begin
6107
     * the search. Offsets may be negative to count from the last character
6108
     * in the string.
6109
     *
6110
     * @param string $str      <p>The input string.</p>
6111
     * @param string $needle   <p>Substring to look for.</p>
6112
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6113
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6114
     *
6115
     * @psalm-pure
6116
     *
6117
     * @return false|int
6118
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6119
     *
6120
     * @see        UTF8::strripos()
6121
     * @deprecated <p>please use "UTF8::strripos()"</p>
6122
     */
6123
    public static function str_iindex_last(
6124
        string $str,
6125
        string $needle,
6126
        int $offset = 0,
6127
        string $encoding = 'UTF-8'
6128
    ) {
6129
        return self::strripos(
6130
            $str,
6131
            $needle,
6132
            $offset,
6133
            $encoding
6134
        );
6135
    }
6136
6137
    /**
6138
     * Returns the index of the first occurrence of $needle in the string,
6139
     * and false if not found. Accepts an optional offset from which to begin
6140
     * the search.
6141
     *
6142
     * @param string $str      <p>The input string.</p>
6143
     * @param string $needle   <p>Substring to look for.</p>
6144
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6145
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6146
     *
6147
     * @psalm-pure
6148
     *
6149
     * @return false|int
6150
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6151
     *
6152
     * @see        UTF8::strpos()
6153
     * @deprecated <p>please use "UTF8::strpos()"</p>
6154
     */
6155 10
    public static function str_index_first(
6156
        string $str,
6157
        string $needle,
6158
        int $offset = 0,
6159
        string $encoding = 'UTF-8'
6160
    ) {
6161 10
        return self::strpos(
6162 10
            $str,
6163 10
            $needle,
6164 10
            $offset,
6165 10
            $encoding
6166
        );
6167
    }
6168
6169
    /**
6170
     * Returns the index of the last occurrence of $needle in the string,
6171
     * and false if not found. Accepts an optional offset from which to begin
6172
     * the search. Offsets may be negative to count from the last character
6173
     * in the string.
6174
     *
6175
     * @param string $str      <p>The input string.</p>
6176
     * @param string $needle   <p>Substring to look for.</p>
6177
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6178
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6179
     *
6180
     * @psalm-pure
6181
     *
6182
     * @return false|int
6183
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6184
     *
6185
     * @see        UTF8::strrpos()
6186
     * @deprecated <p>please use "UTF8::strrpos()"</p>
6187
     */
6188 10
    public static function str_index_last(
6189
        string $str,
6190
        string $needle,
6191
        int $offset = 0,
6192
        string $encoding = 'UTF-8'
6193
    ) {
6194 10
        return self::strrpos(
6195 10
            $str,
6196 10
            $needle,
6197 10
            $offset,
6198 10
            $encoding
6199
        );
6200
    }
6201
6202
    /**
6203
     * Inserts $substring into the string at the $index provided.
6204
     *
6205
     * @param string $str       <p>The input string.</p>
6206
     * @param string $substring <p>String to be inserted.</p>
6207
     * @param int    $index     <p>The index at which to insert the substring.</p>
6208
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
6209
     *
6210
     * @psalm-pure
6211
     *
6212
     * @return string
6213
     */
6214 8
    public static function str_insert(
6215
        string $str,
6216
        string $substring,
6217
        int $index,
6218
        string $encoding = 'UTF-8'
6219
    ): string {
6220 8
        if ($encoding === 'UTF-8') {
6221 4
            $len = (int) \mb_strlen($str);
6222 4
            if ($index > $len) {
6223
                return $str;
6224
            }
6225
6226
            /** @noinspection UnnecessaryCastingInspection */
6227 4
            return (string) \mb_substr($str, 0, $index) .
6228 4
                   $substring .
6229 4
                   (string) \mb_substr($str, $index, $len);
6230
        }
6231
6232 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6233
6234 4
        $len = (int) self::strlen($str, $encoding);
6235 4
        if ($index > $len) {
6236 1
            return $str;
6237
        }
6238
6239 3
        return ((string) self::substr($str, 0, $index, $encoding)) .
6240 3
               $substring .
6241 3
               ((string) self::substr($str, $index, $len, $encoding));
6242
    }
6243
6244
    /**
6245
     * Case-insensitive and UTF-8 safe version of <function>str_replace</function>.
6246
     *
6247
     * @see http://php.net/manual/en/function.str-ireplace.php
6248
     *
6249
     * @param mixed $search  <p>
6250
     *                       Every replacement with search array is
6251
     *                       performed on the result of previous replacement.
6252
     *                       </p>
6253
     * @param mixed $replace <p>
6254
     *                       </p>
6255
     * @param mixed $subject <p>
6256
     *                       If subject is an array, then the search and
6257
     *                       replace is performed with every entry of
6258
     *                       subject, and the return value is an array as
6259
     *                       well.
6260
     *                       </p>
6261
     * @param int   $count   [optional] <p>
6262
     *                       The number of matched and replaced needles will
6263
     *                       be returned in count which is passed by
6264
     *                       reference.
6265
     *                       </p>
6266
     *
6267
     * @psalm-pure
6268
     *
6269
     * @return mixed a string or an array of replacements
6270
     */
6271 29
    public static function str_ireplace($search, $replace, $subject, &$count = null)
6272
    {
6273 29
        $search = (array) $search;
6274
6275
        /** @noinspection AlterInForeachInspection */
6276 29
        foreach ($search as &$s) {
6277 29
            $s = (string) $s;
6278 29
            if ($s === '') {
6279 6
                $s = '/^(?<=.)$/';
6280
            } else {
6281 29
                $s = '/' . \preg_quote($s, '/') . '/ui';
6282
            }
6283
        }
6284
6285 29
        $subject = \preg_replace($search, $replace, $subject, -1, $replace);
6286 29
        $count = $replace; // used as reference parameter
6287
6288 29
        return $subject;
6289
    }
6290
6291
    /**
6292
     * Replaces $search from the beginning of string with $replacement.
6293
     *
6294
     * @param string $str         <p>The input string.</p>
6295
     * @param string $search      <p>The string to search for.</p>
6296
     * @param string $replacement <p>The replacement.</p>
6297
     *
6298
     * @psalm-pure
6299
     *
6300
     * @return string string after the replacements
6301
     */
6302 17
    public static function str_ireplace_beginning(string $str, string $search, string $replacement): string
6303
    {
6304 17
        if ($str === '') {
6305 4
            if ($replacement === '') {
6306 2
                return '';
6307
            }
6308
6309 2
            if ($search === '') {
6310 2
                return $replacement;
6311
            }
6312
        }
6313
6314 13
        if ($search === '') {
6315 2
            return $str . $replacement;
6316
        }
6317
6318 11
        if (\stripos($str, $search) === 0) {
6319 10
            return $replacement . \substr($str, \strlen($search));
6320
        }
6321
6322 1
        return $str;
6323
    }
6324
6325
    /**
6326
     * Replaces $search from the ending of string with $replacement.
6327
     *
6328
     * @param string $str         <p>The input string.</p>
6329
     * @param string $search      <p>The string to search for.</p>
6330
     * @param string $replacement <p>The replacement.</p>
6331
     *
6332
     * @psalm-pure
6333
     *
6334
     * @return string
6335
     *                <p>string after the replacements.</p>
6336
     */
6337 17
    public static function str_ireplace_ending(string $str, string $search, string $replacement): string
6338
    {
6339 17
        if ($str === '') {
6340 4
            if ($replacement === '') {
6341 2
                return '';
6342
            }
6343
6344 2
            if ($search === '') {
6345 2
                return $replacement;
6346
            }
6347
        }
6348
6349 13
        if ($search === '') {
6350 2
            return $str . $replacement;
6351
        }
6352
6353 11
        if (\stripos($str, $search, \strlen($str) - \strlen($search)) !== false) {
6354 9
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
6355
        }
6356
6357 11
        return $str;
6358
    }
6359
6360
    /**
6361
     * Check if the string starts with the given substring, case-insensitive.
6362
     *
6363
     * @param string $haystack <p>The string to search in.</p>
6364
     * @param string $needle   <p>The substring to search for.</p>
6365
     *
6366
     * @psalm-pure
6367
     *
6368
     * @return bool
6369
     */
6370 12
    public static function str_istarts_with(string $haystack, string $needle): bool
6371
    {
6372 12
        if ($needle === '') {
6373 2
            return true;
6374
        }
6375
6376 12
        if ($haystack === '') {
6377
            return false;
6378
        }
6379
6380 12
        return self::stripos($haystack, $needle) === 0;
6381
    }
6382
6383
    /**
6384
     * Returns true if the string begins with any of $substrings, false otherwise.
6385
     *
6386
     * - case-insensitive
6387
     *
6388
     * @param string $str        <p>The input string.</p>
6389
     * @param array  $substrings <p>Substrings to look for.</p>
6390
     *
6391
     * @psalm-pure
6392
     *
6393
     * @return bool whether or not $str starts with $substring
6394
     */
6395 4
    public static function str_istarts_with_any(string $str, array $substrings): bool
6396
    {
6397 4
        if ($str === '') {
6398
            return false;
6399
        }
6400
6401 4
        if ($substrings === []) {
6402
            return false;
6403
        }
6404
6405 4
        foreach ($substrings as &$substring) {
6406 4
            if (self::str_istarts_with($str, $substring)) {
6407 4
                return true;
6408
            }
6409
        }
6410
6411
        return false;
6412
    }
6413
6414
    /**
6415
     * Gets the substring after the first occurrence of a separator.
6416
     *
6417
     * @param string $str       <p>The input string.</p>
6418
     * @param string $separator <p>The string separator.</p>
6419
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6420
     *
6421
     * @psalm-pure
6422
     *
6423
     * @return string
6424
     */
6425 1
    public static function str_isubstr_after_first_separator(
6426
        string $str,
6427
        string $separator,
6428
        string $encoding = 'UTF-8'
6429
    ): string {
6430 1
        if ($separator === '' || $str === '') {
6431 1
            return '';
6432
        }
6433
6434 1
        $offset = self::stripos($str, $separator);
6435 1
        if ($offset === false) {
6436 1
            return '';
6437
        }
6438
6439 1
        if ($encoding === 'UTF-8') {
6440 1
            return (string) \mb_substr(
6441 1
                $str,
6442 1
                $offset + (int) \mb_strlen($separator)
6443
            );
6444
        }
6445
6446
        return (string) self::substr(
6447
            $str,
6448
            $offset + (int) self::strlen($separator, $encoding),
6449
            null,
6450
            $encoding
6451
        );
6452
    }
6453
6454
    /**
6455
     * Gets the substring after the last occurrence of a separator.
6456
     *
6457
     * @param string $str       <p>The input string.</p>
6458
     * @param string $separator <p>The string separator.</p>
6459
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6460
     *
6461
     * @psalm-pure
6462
     *
6463
     * @return string
6464
     */
6465 1
    public static function str_isubstr_after_last_separator(
6466
        string $str,
6467
        string $separator,
6468
        string $encoding = 'UTF-8'
6469
    ): string {
6470 1
        if ($separator === '' || $str === '') {
6471 1
            return '';
6472
        }
6473
6474 1
        $offset = self::strripos($str, $separator);
6475 1
        if ($offset === false) {
6476 1
            return '';
6477
        }
6478
6479 1
        if ($encoding === 'UTF-8') {
6480 1
            return (string) \mb_substr(
6481 1
                $str,
6482 1
                $offset + (int) self::strlen($separator)
6483
            );
6484
        }
6485
6486
        return (string) self::substr(
6487
            $str,
6488
            $offset + (int) self::strlen($separator, $encoding),
6489
            null,
6490
            $encoding
6491
        );
6492
    }
6493
6494
    /**
6495
     * Gets the substring before the first occurrence of a separator.
6496
     *
6497
     * @param string $str       <p>The input string.</p>
6498
     * @param string $separator <p>The string separator.</p>
6499
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6500
     *
6501
     * @psalm-pure
6502
     *
6503
     * @return string
6504
     */
6505 1
    public static function str_isubstr_before_first_separator(
6506
        string $str,
6507
        string $separator,
6508
        string $encoding = 'UTF-8'
6509
    ): string {
6510 1
        if ($separator === '' || $str === '') {
6511 1
            return '';
6512
        }
6513
6514 1
        $offset = self::stripos($str, $separator);
6515 1
        if ($offset === false) {
6516 1
            return '';
6517
        }
6518
6519 1
        if ($encoding === 'UTF-8') {
6520 1
            return (string) \mb_substr($str, 0, $offset);
6521
        }
6522
6523
        return (string) self::substr($str, 0, $offset, $encoding);
6524
    }
6525
6526
    /**
6527
     * Gets the substring before the last occurrence of a separator.
6528
     *
6529
     * @param string $str       <p>The input string.</p>
6530
     * @param string $separator <p>The string separator.</p>
6531
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6532
     *
6533
     * @psalm-pure
6534
     *
6535
     * @return string
6536
     */
6537 1
    public static function str_isubstr_before_last_separator(
6538
        string $str,
6539
        string $separator,
6540
        string $encoding = 'UTF-8'
6541
    ): string {
6542 1
        if ($separator === '' || $str === '') {
6543 1
            return '';
6544
        }
6545
6546 1
        if ($encoding === 'UTF-8') {
6547 1
            $offset = \mb_strripos($str, $separator);
6548 1
            if ($offset === false) {
6549 1
                return '';
6550
            }
6551
6552 1
            return (string) \mb_substr($str, 0, $offset);
6553
        }
6554
6555
        $offset = self::strripos($str, $separator, 0, $encoding);
6556
        if ($offset === false) {
6557
            return '';
6558
        }
6559
6560
        return (string) self::substr($str, 0, $offset, $encoding);
6561
    }
6562
6563
    /**
6564
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
6565
     *
6566
     * @param string $str           <p>The input string.</p>
6567
     * @param string $needle        <p>The string to look for.</p>
6568
     * @param bool   $before_needle [optional] <p>Default: false</p>
6569
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6570
     *
6571
     * @psalm-pure
6572
     *
6573
     * @return string
6574
     */
6575 2
    public static function str_isubstr_first(
6576
        string $str,
6577
        string $needle,
6578
        bool $before_needle = false,
6579
        string $encoding = 'UTF-8'
6580
    ): string {
6581
        if (
6582 2
            $needle === ''
6583
            ||
6584 2
            $str === ''
6585
        ) {
6586 2
            return '';
6587
        }
6588
6589 2
        $part = self::stristr(
6590 2
            $str,
6591 2
            $needle,
6592 2
            $before_needle,
6593 2
            $encoding
6594
        );
6595 2
        if ($part === false) {
6596 2
            return '';
6597
        }
6598
6599 2
        return $part;
6600
    }
6601
6602
    /**
6603
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
6604
     *
6605
     * @param string $str           <p>The input string.</p>
6606
     * @param string $needle        <p>The string to look for.</p>
6607
     * @param bool   $before_needle [optional] <p>Default: false</p>
6608
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6609
     *
6610
     * @psalm-pure
6611
     *
6612
     * @return string
6613
     */
6614 1
    public static function str_isubstr_last(
6615
        string $str,
6616
        string $needle,
6617
        bool $before_needle = false,
6618
        string $encoding = 'UTF-8'
6619
    ): string {
6620
        if (
6621 1
            $needle === ''
6622
            ||
6623 1
            $str === ''
6624
        ) {
6625 1
            return '';
6626
        }
6627
6628 1
        $part = self::strrichr(
6629 1
            $str,
6630 1
            $needle,
6631 1
            $before_needle,
6632 1
            $encoding
6633
        );
6634 1
        if ($part === false) {
6635 1
            return '';
6636
        }
6637
6638 1
        return $part;
6639
    }
6640
6641
    /**
6642
     * Returns the last $n characters of the string.
6643
     *
6644
     * @param string $str      <p>The input string.</p>
6645
     * @param int    $n        <p>Number of characters to retrieve from the end.</p>
6646
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6647
     *
6648
     * @psalm-pure
6649
     *
6650
     * @return string
6651
     */
6652 12
    public static function str_last_char(
6653
        string $str,
6654
        int $n = 1,
6655
        string $encoding = 'UTF-8'
6656
    ): string {
6657 12
        if ($str === '' || $n <= 0) {
6658 4
            return '';
6659
        }
6660
6661 8
        if ($encoding === 'UTF-8') {
6662 4
            return (string) \mb_substr($str, -$n);
6663
        }
6664
6665 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6666
6667 4
        return (string) self::substr($str, -$n, null, $encoding);
6668
    }
6669
6670
    /**
6671
     * Limit the number of characters in a string.
6672
     *
6673
     * @param string $str        <p>The input string.</p>
6674
     * @param int    $length     [optional] <p>Default: 100</p>
6675
     * @param string $str_add_on [optional] <p>Default: …</p>
6676
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6677
     *
6678
     * @psalm-pure
6679
     *
6680
     * @return string
6681
     */
6682 2
    public static function str_limit(
6683
        string $str,
6684
        int $length = 100,
6685
        string $str_add_on = '…',
6686
        string $encoding = 'UTF-8'
6687
    ): string {
6688 2
        if ($str === '' || $length <= 0) {
6689 2
            return '';
6690
        }
6691
6692 2
        if ($encoding === 'UTF-8') {
6693 2
            if ((int) \mb_strlen($str) <= $length) {
6694 2
                return $str;
6695
            }
6696
6697
            /** @noinspection UnnecessaryCastingInspection */
6698 2
            return (string) \mb_substr($str, 0, $length - (int) self::strlen($str_add_on)) . $str_add_on;
6699
        }
6700
6701
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6702
6703
        if ((int) self::strlen($str, $encoding) <= $length) {
6704
            return $str;
6705
        }
6706
6707
        return ((string) self::substr($str, 0, $length - (int) self::strlen($str_add_on), $encoding)) . $str_add_on;
6708
    }
6709
6710
    /**
6711
     * Limit the number of characters in a string, but also after the next word.
6712
     *
6713
     * @param string $str        <p>The input string.</p>
6714
     * @param int    $length     [optional] <p>Default: 100</p>
6715
     * @param string $str_add_on [optional] <p>Default: …</p>
6716
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6717
     *
6718
     * @psalm-pure
6719
     *
6720
     * @return string
6721
     */
6722 6
    public static function str_limit_after_word(
6723
        string $str,
6724
        int $length = 100,
6725
        string $str_add_on = '…',
6726
        string $encoding = 'UTF-8'
6727
    ): string {
6728 6
        if ($str === '' || $length <= 0) {
6729 2
            return '';
6730
        }
6731
6732 6
        if ($encoding === 'UTF-8') {
6733
            /** @noinspection UnnecessaryCastingInspection */
6734 2
            if ((int) \mb_strlen($str) <= $length) {
6735 2
                return $str;
6736
            }
6737
6738 2
            if (\mb_substr($str, $length - 1, 1) === ' ') {
6739 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6740
            }
6741
6742 2
            $str = \mb_substr($str, 0, $length);
6743
6744 2
            $array = \explode(' ', $str);
6745 2
            \array_pop($array);
6746 2
            $new_str = \implode(' ', $array);
6747
6748 2
            if ($new_str === '') {
6749 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6750
            }
6751
        } else {
6752 4
            if ((int) self::strlen($str, $encoding) <= $length) {
6753
                return $str;
6754
            }
6755
6756 4
            if (self::substr($str, $length - 1, 1, $encoding) === ' ') {
6757 3
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6758
            }
6759
6760
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6761 1
            $str = self::substr($str, 0, $length, $encoding);
6762
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6763 1
            if ($str === false) {
6764
                return '' . $str_add_on;
6765
            }
6766
6767 1
            $array = \explode(' ', $str);
6768 1
            \array_pop($array);
6769 1
            $new_str = \implode(' ', $array);
6770
6771 1
            if ($new_str === '') {
6772
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6773
            }
6774
        }
6775
6776 3
        return $new_str . $str_add_on;
6777
    }
6778
6779
    /**
6780
     * Returns the longest common prefix between the $str1 and $str2.
6781
     *
6782
     * @param string $str1     <p>The input sting.</p>
6783
     * @param string $str2     <p>Second string for comparison.</p>
6784
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6785
     *
6786
     * @psalm-pure
6787
     *
6788
     * @return string
6789
     */
6790 10
    public static function str_longest_common_prefix(
6791
        string $str1,
6792
        string $str2,
6793
        string $encoding = 'UTF-8'
6794
    ): string {
6795
        // init
6796 10
        $longest_common_prefix = '';
6797
6798 10
        if ($encoding === 'UTF-8') {
6799 5
            $max_length = (int) \min(
6800 5
                \mb_strlen($str1),
6801 5
                \mb_strlen($str2)
6802
            );
6803
6804 5
            for ($i = 0; $i < $max_length; ++$i) {
6805 4
                $char = \mb_substr($str1, $i, 1);
6806
6807
                if (
6808 4
                    $char !== false
6809
                    &&
6810 4
                    $char === \mb_substr($str2, $i, 1)
6811
                ) {
6812 3
                    $longest_common_prefix .= $char;
6813
                } else {
6814 3
                    break;
6815
                }
6816
            }
6817
        } else {
6818 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6819
6820 5
            $max_length = (int) \min(
6821 5
                self::strlen($str1, $encoding),
6822 5
                self::strlen($str2, $encoding)
6823
            );
6824
6825 5
            for ($i = 0; $i < $max_length; ++$i) {
6826 4
                $char = self::substr($str1, $i, 1, $encoding);
6827
6828
                if (
6829 4
                    $char !== false
6830
                    &&
6831 4
                    $char === self::substr($str2, $i, 1, $encoding)
6832
                ) {
6833 3
                    $longest_common_prefix .= $char;
6834
                } else {
6835 3
                    break;
6836
                }
6837
            }
6838
        }
6839
6840 10
        return $longest_common_prefix;
6841
    }
6842
6843
    /**
6844
     * Returns the longest common substring between the $str1 and $str2.
6845
     * In the case of ties, it returns that which occurs first.
6846
     *
6847
     * @param string $str1
6848
     * @param string $str2     <p>Second string for comparison.</p>
6849
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6850
     *
6851
     * @psalm-pure
6852
     *
6853
     * @return string
6854
     *                <p>A string with its $str being the longest common substring.</p>
6855
     */
6856 11
    public static function str_longest_common_substring(
6857
        string $str1,
6858
        string $str2,
6859
        string $encoding = 'UTF-8'
6860
    ): string {
6861 11
        if ($str1 === '' || $str2 === '') {
6862 2
            return '';
6863
        }
6864
6865
        // Uses dynamic programming to solve
6866
        // http://en.wikipedia.org/wiki/Longest_common_substring_problem
6867
6868 9
        if ($encoding === 'UTF-8') {
6869 4
            $str_length = (int) \mb_strlen($str1);
6870 4
            $other_length = (int) \mb_strlen($str2);
6871
        } else {
6872 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6873
6874 5
            $str_length = (int) self::strlen($str1, $encoding);
6875 5
            $other_length = (int) self::strlen($str2, $encoding);
6876
        }
6877
6878
        // Return if either string is empty
6879 9
        if ($str_length === 0 || $other_length === 0) {
6880
            return '';
6881
        }
6882
6883 9
        $len = 0;
6884 9
        $end = 0;
6885 9
        $table = \array_fill(
6886 9
            0,
6887 9
            $str_length + 1,
6888 9
            \array_fill(0, $other_length + 1, 0)
6889
        );
6890
6891 9
        if ($encoding === 'UTF-8') {
6892 9
            for ($i = 1; $i <= $str_length; ++$i) {
6893 9
                for ($j = 1; $j <= $other_length; ++$j) {
6894 9
                    $str_char = \mb_substr($str1, $i - 1, 1);
6895 9
                    $other_char = \mb_substr($str2, $j - 1, 1);
6896
6897 9
                    if ($str_char === $other_char) {
6898 8
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6899 8
                        if ($table[$i][$j] > $len) {
6900 8
                            $len = $table[$i][$j];
6901 8
                            $end = $i;
6902
                        }
6903
                    } else {
6904 9
                        $table[$i][$j] = 0;
6905
                    }
6906
                }
6907
            }
6908
        } else {
6909
            for ($i = 1; $i <= $str_length; ++$i) {
6910
                for ($j = 1; $j <= $other_length; ++$j) {
6911
                    $str_char = self::substr($str1, $i - 1, 1, $encoding);
6912
                    $other_char = self::substr($str2, $j - 1, 1, $encoding);
6913
6914
                    if ($str_char === $other_char) {
6915
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6916
                        if ($table[$i][$j] > $len) {
6917
                            $len = $table[$i][$j];
6918
                            $end = $i;
6919
                        }
6920
                    } else {
6921
                        $table[$i][$j] = 0;
6922
                    }
6923
                }
6924
            }
6925
        }
6926
6927 9
        if ($encoding === 'UTF-8') {
6928 9
            return (string) \mb_substr($str1, $end - $len, $len);
6929
        }
6930
6931
        return (string) self::substr($str1, $end - $len, $len, $encoding);
6932
    }
6933
6934
    /**
6935
     * Returns the longest common suffix between the $str1 and $str2.
6936
     *
6937
     * @param string $str1
6938
     * @param string $str2     <p>Second string for comparison.</p>
6939
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6940
     *
6941
     * @psalm-pure
6942
     *
6943
     * @return string
6944
     */
6945 10
    public static function str_longest_common_suffix(
6946
        string $str1,
6947
        string $str2,
6948
        string $encoding = 'UTF-8'
6949
    ): string {
6950 10
        if ($str1 === '' || $str2 === '') {
6951 2
            return '';
6952
        }
6953
6954 8
        if ($encoding === 'UTF-8') {
6955 4
            $max_length = (int) \min(
6956 4
                \mb_strlen($str1, $encoding),
6957 4
                \mb_strlen($str2, $encoding)
6958
            );
6959
6960 4
            $longest_common_suffix = '';
6961 4
            for ($i = 1; $i <= $max_length; ++$i) {
6962 4
                $char = \mb_substr($str1, -$i, 1);
6963
6964
                if (
6965 4
                    $char !== false
6966
                    &&
6967 4
                    $char === \mb_substr($str2, -$i, 1)
6968
                ) {
6969 3
                    $longest_common_suffix = $char . $longest_common_suffix;
6970
                } else {
6971 3
                    break;
6972
                }
6973
            }
6974
        } else {
6975 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6976
6977 4
            $max_length = (int) \min(
6978 4
                self::strlen($str1, $encoding),
6979 4
                self::strlen($str2, $encoding)
6980
            );
6981
6982 4
            $longest_common_suffix = '';
6983 4
            for ($i = 1; $i <= $max_length; ++$i) {
6984 4
                $char = self::substr($str1, -$i, 1, $encoding);
6985
6986
                if (
6987 4
                    $char !== false
6988
                    &&
6989 4
                    $char === self::substr($str2, -$i, 1, $encoding)
6990
                ) {
6991 3
                    $longest_common_suffix = $char . $longest_common_suffix;
6992
                } else {
6993 3
                    break;
6994
                }
6995
            }
6996
        }
6997
6998 8
        return $longest_common_suffix;
6999
    }
7000
7001
    /**
7002
     * Returns true if $str matches the supplied pattern, false otherwise.
7003
     *
7004
     * @param string $str     <p>The input string.</p>
7005
     * @param string $pattern <p>Regex pattern to match against.</p>
7006
     *
7007
     * @psalm-pure
7008
     *
7009
     * @return bool whether or not $str matches the pattern
7010
     */
7011 10
    public static function str_matches_pattern(string $str, string $pattern): bool
7012
    {
7013 10
        return (bool) \preg_match('/' . $pattern . '/u', $str);
7014
    }
7015
7016
    /**
7017
     * Returns whether or not a character exists at an index. Offsets may be
7018
     * negative to count from the last character in the string. Implements
7019
     * part of the ArrayAccess interface.
7020
     *
7021
     * @param string $str      <p>The input string.</p>
7022
     * @param int    $offset   <p>The index to check.</p>
7023
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7024
     *
7025
     * @psalm-pure
7026
     *
7027
     * @return bool whether or not the index exists
7028
     */
7029 6
    public static function str_offset_exists(string $str, int $offset, string $encoding = 'UTF-8'): bool
7030
    {
7031
        // init
7032 6
        $length = (int) self::strlen($str, $encoding);
7033
7034 6
        if ($offset >= 0) {
7035 3
            return $length > $offset;
7036
        }
7037
7038 3
        return $length >= \abs($offset);
7039
    }
7040
7041
    /**
7042
     * Returns the character at the given index. Offsets may be negative to
7043
     * count from the last character in the string. Implements part of the
7044
     * ArrayAccess interface, and throws an OutOfBoundsException if the index
7045
     * does not exist.
7046
     *
7047
     * @param string $str      <p>The input string.</p>
7048
     * @param int    $index    <p>The <strong>index</strong> from which to retrieve the char.</p>
7049
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7050
     *
7051
     * @throws \OutOfBoundsException if the positive or negative offset does not exist
7052
     *
7053
     * @return string
7054
     *                <p>The character at the specified index.</p>
7055
     *
7056
     * @psalm-pure
7057
     */
7058 2
    public static function str_offset_get(string $str, int $index, string $encoding = 'UTF-8'): string
7059
    {
7060
        // init
7061 2
        $length = (int) self::strlen($str);
7062
7063
        if (
7064 2
            ($index >= 0 && $length <= $index)
7065
            ||
7066 2
            $length < \abs($index)
7067
        ) {
7068 1
            throw new \OutOfBoundsException('No character exists at the index');
7069
        }
7070
7071 1
        return self::char_at($str, $index, $encoding);
7072
    }
7073
7074
    /**
7075
     * Pad a UTF-8 string to a given length with another string.
7076
     *
7077
     * @param string     $str        <p>The input string.</p>
7078
     * @param int        $pad_length <p>The length of return string.</p>
7079
     * @param string     $pad_string [optional] <p>String to use for padding the input string.</p>
7080
     * @param int|string $pad_type   [optional] <p>
7081
     *                               Can be <strong>STR_PAD_RIGHT</strong> (default), [or string "right"]<br>
7082
     *                               <strong>STR_PAD_LEFT</strong> [or string "left"] or<br>
7083
     *                               <strong>STR_PAD_BOTH</strong> [or string "both"]
7084
     *                               </p>
7085
     * @param string     $encoding   [optional] <p>Default: 'UTF-8'</p>
7086
     *
7087
     * @psalm-pure
7088
     *
7089
     * @return string
7090
     *                <p>Returns the padded string.</p>
7091
     */
7092 41
    public static function str_pad(
7093
        string $str,
7094
        int $pad_length,
7095
        string $pad_string = ' ',
7096
        $pad_type = \STR_PAD_RIGHT,
7097
        string $encoding = 'UTF-8'
7098
    ): string {
7099 41
        if ($pad_length === 0 || $pad_string === '') {
7100 1
            return $str;
7101
        }
7102
7103 41
        if ($pad_type !== (int) $pad_type) {
7104 13
            if ($pad_type === 'left') {
7105 3
                $pad_type = \STR_PAD_LEFT;
7106 10
            } elseif ($pad_type === 'right') {
7107 6
                $pad_type = \STR_PAD_RIGHT;
7108 4
            } elseif ($pad_type === 'both') {
7109 3
                $pad_type = \STR_PAD_BOTH;
7110
            } else {
7111 1
                throw new \InvalidArgumentException(
7112 1
                    'Pad expects $pad_type to be "STR_PAD_*" or ' . "to be one of 'left', 'right' or 'both'"
7113
                );
7114
            }
7115
        }
7116
7117 40
        if ($encoding === 'UTF-8') {
7118 25
            $str_length = (int) \mb_strlen($str);
7119
7120 25
            if ($pad_length >= $str_length) {
7121
                switch ($pad_type) {
7122 25
                    case \STR_PAD_LEFT:
7123 8
                        $ps_length = (int) \mb_strlen($pad_string);
7124
7125 8
                        $diff = ($pad_length - $str_length);
7126
7127 8
                        $pre = (string) \mb_substr(
7128 8
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7129 8
                            0,
7130 8
                            $diff
7131
                        );
7132 8
                        $post = '';
7133
7134 8
                        break;
7135
7136 20
                    case \STR_PAD_BOTH:
7137 14
                        $diff = ($pad_length - $str_length);
7138
7139 14
                        $ps_length_left = (int) \floor($diff / 2);
7140
7141 14
                        $ps_length_right = (int) \ceil($diff / 2);
7142
7143 14
                        $pre = (string) \mb_substr(
7144 14
                            \str_repeat($pad_string, $ps_length_left),
7145 14
                            0,
7146 14
                            $ps_length_left
7147
                        );
7148 14
                        $post = (string) \mb_substr(
7149 14
                            \str_repeat($pad_string, $ps_length_right),
7150 14
                            0,
7151 14
                            $ps_length_right
7152
                        );
7153
7154 14
                        break;
7155
7156 9
                    case \STR_PAD_RIGHT:
7157
                    default:
7158 9
                        $ps_length = (int) \mb_strlen($pad_string);
7159
7160 9
                        $diff = ($pad_length - $str_length);
7161
7162 9
                        $post = (string) \mb_substr(
7163 9
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7164 9
                            0,
7165 9
                            $diff
7166
                        );
7167 9
                        $pre = '';
7168
                }
7169
7170 25
                return $pre . $str . $post;
7171
            }
7172
7173 3
            return $str;
7174
        }
7175
7176 15
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7177
7178 15
        $str_length = (int) self::strlen($str, $encoding);
7179
7180 15
        if ($pad_length >= $str_length) {
7181
            switch ($pad_type) {
7182 14
                case \STR_PAD_LEFT:
7183 5
                    $ps_length = (int) self::strlen($pad_string, $encoding);
7184
7185 5
                    $diff = ($pad_length - $str_length);
7186
7187 5
                    $pre = (string) self::substr(
7188 5
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7189 5
                        0,
7190 5
                        $diff,
7191 5
                        $encoding
7192
                    );
7193 5
                    $post = '';
7194
7195 5
                    break;
7196
7197 9
                case \STR_PAD_BOTH:
7198 3
                    $diff = ($pad_length - $str_length);
7199
7200 3
                    $ps_length_left = (int) \floor($diff / 2);
7201
7202 3
                    $ps_length_right = (int) \ceil($diff / 2);
7203
7204 3
                    $pre = (string) self::substr(
7205 3
                        \str_repeat($pad_string, $ps_length_left),
7206 3
                        0,
7207 3
                        $ps_length_left,
7208 3
                        $encoding
7209
                    );
7210 3
                    $post = (string) self::substr(
7211 3
                        \str_repeat($pad_string, $ps_length_right),
7212 3
                        0,
7213 3
                        $ps_length_right,
7214 3
                        $encoding
7215
                    );
7216
7217 3
                    break;
7218
7219 6
                case \STR_PAD_RIGHT:
7220
                default:
7221 6
                    $ps_length = (int) self::strlen($pad_string, $encoding);
7222
7223 6
                    $diff = ($pad_length - $str_length);
7224
7225 6
                    $post = (string) self::substr(
7226 6
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7227 6
                        0,
7228 6
                        $diff,
7229 6
                        $encoding
7230
                    );
7231 6
                    $pre = '';
7232
            }
7233
7234 14
            return $pre . $str . $post;
7235
        }
7236
7237 1
        return $str;
7238
    }
7239
7240
    /**
7241
     * Returns a new string of a given length such that both sides of the
7242
     * string are padded. Alias for "UTF8::str_pad()" with a $pad_type of 'both'.
7243
     *
7244
     * @param string $str
7245
     * @param int    $length   <p>Desired string length after padding.</p>
7246
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7247
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7248
     *
7249
     * @psalm-pure
7250
     *
7251
     * @return string
7252
     *                <p>The string with padding applied.</p>
7253
     */
7254 11
    public static function str_pad_both(
7255
        string $str,
7256
        int $length,
7257
        string $pad_str = ' ',
7258
        string $encoding = 'UTF-8'
7259
    ): string {
7260 11
        return self::str_pad(
7261 11
            $str,
7262 11
            $length,
7263 11
            $pad_str,
7264 11
            \STR_PAD_BOTH,
7265 11
            $encoding
7266
        );
7267
    }
7268
7269
    /**
7270
     * Returns a new string of a given length such that the beginning of the
7271
     * string is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'left'.
7272
     *
7273
     * @param string $str
7274
     * @param int    $length   <p>Desired string length after padding.</p>
7275
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7276
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7277
     *
7278
     * @psalm-pure
7279
     *
7280
     * @return string
7281
     *                <p>The string with left padding.</p>
7282
     */
7283 7
    public static function str_pad_left(
7284
        string $str,
7285
        int $length,
7286
        string $pad_str = ' ',
7287
        string $encoding = 'UTF-8'
7288
    ): string {
7289 7
        return self::str_pad(
7290 7
            $str,
7291 7
            $length,
7292 7
            $pad_str,
7293 7
            \STR_PAD_LEFT,
7294 7
            $encoding
7295
        );
7296
    }
7297
7298
    /**
7299
     * Returns a new string of a given length such that the end of the string
7300
     * is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'right'.
7301
     *
7302
     * @param string $str
7303
     * @param int    $length   <p>Desired string length after padding.</p>
7304
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7305
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7306
     *
7307
     * @psalm-pure
7308
     *
7309
     * @return string
7310
     *                <p>The string with right padding.</p>
7311
     */
7312 7
    public static function str_pad_right(
7313
        string $str,
7314
        int $length,
7315
        string $pad_str = ' ',
7316
        string $encoding = 'UTF-8'
7317
    ): string {
7318 7
        return self::str_pad(
7319 7
            $str,
7320 7
            $length,
7321 7
            $pad_str,
7322 7
            \STR_PAD_RIGHT,
7323 7
            $encoding
7324
        );
7325
    }
7326
7327
    /**
7328
     * Repeat a string.
7329
     *
7330
     * @param string $str        <p>
7331
     *                           The string to be repeated.
7332
     *                           </p>
7333
     * @param int    $multiplier <p>
7334
     *                           Number of time the input string should be
7335
     *                           repeated.
7336
     *                           </p>
7337
     *                           <p>
7338
     *                           multiplier has to be greater than or equal to 0.
7339
     *                           If the multiplier is set to 0, the function
7340
     *                           will return an empty string.
7341
     *                           </p>
7342
     *
7343
     * @psalm-pure
7344
     *
7345
     * @return string
7346
     *                <p>The repeated string.</P>
7347
     */
7348 9
    public static function str_repeat(string $str, int $multiplier): string
7349
    {
7350 9
        $str = self::filter($str);
7351
7352 9
        return \str_repeat($str, $multiplier);
7353
    }
7354
7355
    /**
7356
     * INFO: This is only a wrapper for "str_replace()"  -> the original functions is already UTF-8 safe.
7357
     *
7358
     * Replace all occurrences of the search string with the replacement string
7359
     *
7360
     * @see http://php.net/manual/en/function.str-replace.php
7361
     *
7362
     * @param mixed $search  <p>
7363
     *                       The value being searched for, otherwise known as the needle.
7364
     *                       An array may be used to designate multiple needles.
7365
     *                       </p>
7366
     * @param mixed $replace <p>
7367
     *                       The replacement value that replaces found search
7368
     *                       values. An array may be used to designate multiple replacements.
7369
     *                       </p>
7370
     * @param mixed $subject <p>
7371
     *                       The string or array being searched and replaced on,
7372
     *                       otherwise known as the haystack.
7373
     *                       </p>
7374
     *                       <p>
7375
     *                       If subject is an array, then the search and
7376
     *                       replace is performed with every entry of
7377
     *                       subject, and the return value is an array as
7378
     *                       well.
7379
     *                       </p>
7380
     * @param int   $count   [optional] If passed, this will hold the number of matched and replaced needles
7381
     *
7382
     * @psalm-pure
7383
     *
7384
     * @return mixed this function returns a string or an array with the replaced values
7385
     */
7386 12
    public static function str_replace(
7387
        $search,
7388
        $replace,
7389
        $subject,
7390
        int &$count = null
7391
    ) {
7392
        /**
7393
         * @psalm-suppress PossiblyNullArgument
7394
         */
7395 12
        return \str_replace(
7396 12
            $search,
7397 12
            $replace,
7398 12
            $subject,
7399 12
            $count
7400
        );
7401
    }
7402
7403
    /**
7404
     * Replaces $search from the beginning of string with $replacement.
7405
     *
7406
     * @param string $str         <p>The input string.</p>
7407
     * @param string $search      <p>The string to search for.</p>
7408
     * @param string $replacement <p>The replacement.</p>
7409
     *
7410
     * @psalm-pure
7411
     *
7412
     * @return string
7413
     *                <p>A string after the replacements.</p>
7414
     */
7415 17
    public static function str_replace_beginning(
7416
        string $str,
7417
        string $search,
7418
        string $replacement
7419
    ): string {
7420 17
        if ($str === '') {
7421 4
            if ($replacement === '') {
7422 2
                return '';
7423
            }
7424
7425 2
            if ($search === '') {
7426 2
                return $replacement;
7427
            }
7428
        }
7429
7430 13
        if ($search === '') {
7431 2
            return $str . $replacement;
7432
        }
7433
7434 11
        if (\strpos($str, $search) === 0) {
7435 9
            return $replacement . \substr($str, \strlen($search));
7436
        }
7437
7438 2
        return $str;
7439
    }
7440
7441
    /**
7442
     * Replaces $search from the ending of string with $replacement.
7443
     *
7444
     * @param string $str         <p>The input string.</p>
7445
     * @param string $search      <p>The string to search for.</p>
7446
     * @param string $replacement <p>The replacement.</p>
7447
     *
7448
     * @psalm-pure
7449
     *
7450
     * @return string
7451
     *                <p>A string after the replacements.</p>
7452
     */
7453 17
    public static function str_replace_ending(
7454
        string $str,
7455
        string $search,
7456
        string $replacement
7457
    ): string {
7458 17
        if ($str === '') {
7459 4
            if ($replacement === '') {
7460 2
                return '';
7461
            }
7462
7463 2
            if ($search === '') {
7464 2
                return $replacement;
7465
            }
7466
        }
7467
7468 13
        if ($search === '') {
7469 2
            return $str . $replacement;
7470
        }
7471
7472 11
        if (\strpos($str, $search, \strlen($str) - \strlen($search)) !== false) {
7473 8
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
7474
        }
7475
7476 11
        return $str;
7477
    }
7478
7479
    /**
7480
     * Replace the first "$search"-term with the "$replace"-term.
7481
     *
7482
     * @param string $search
7483
     * @param string $replace
7484
     * @param string $subject
7485
     *
7486
     * @psalm-pure
7487
     *
7488
     * @return string
7489
     *
7490
     * @psalm-suppress InvalidReturnType
7491
     */
7492 2
    public static function str_replace_first(
7493
        string $search,
7494
        string $replace,
7495
        string $subject
7496
    ): string {
7497 2
        $pos = self::strpos($subject, $search);
7498
7499 2
        if ($pos !== false) {
7500
            /**
7501
             * @psalm-suppress InvalidReturnStatement
7502
             */
7503 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...
7504 2
                $subject,
7505 2
                $replace,
7506 2
                $pos,
7507 2
                (int) self::strlen($search)
7508
            );
7509
        }
7510
7511 2
        return $subject;
7512
    }
7513
7514
    /**
7515
     * Replace the last "$search"-term with the "$replace"-term.
7516
     *
7517
     * @param string $search
7518
     * @param string $replace
7519
     * @param string $subject
7520
     *
7521
     * @psalm-pure
7522
     *
7523
     * @return string
7524
     *
7525
     * @psalm-suppress InvalidReturnType
7526
     */
7527 2
    public static function str_replace_last(
7528
        string $search,
7529
        string $replace,
7530
        string $subject
7531
    ): string {
7532 2
        $pos = self::strrpos($subject, $search);
7533 2
        if ($pos !== false) {
7534
            /**
7535
             * @psalm-suppress InvalidReturnStatement
7536
             */
7537 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...
7538 2
                $subject,
7539 2
                $replace,
7540 2
                $pos,
7541 2
                (int) self::strlen($search)
7542
            );
7543
        }
7544
7545 2
        return $subject;
7546
    }
7547
7548
    /**
7549
     * Shuffles all the characters in the string.
7550
     *
7551
     * PS: uses random algorithm which is weak for cryptography purposes
7552
     *
7553
     * @param string $str      <p>The input string</p>
7554
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7555
     *
7556
     * @psalm-pure
7557
     *
7558
     * @return string
7559
     *                <p>The shuffled string.</p>
7560
     */
7561 5
    public static function str_shuffle(string $str, string $encoding = 'UTF-8'): string
7562
    {
7563 5
        if ($encoding === 'UTF-8') {
7564 5
            $indexes = \range(0, (int) \mb_strlen($str) - 1);
7565
            /** @noinspection NonSecureShuffleUsageInspection */
7566 5
            \shuffle($indexes);
7567
7568
            // init
7569 5
            $shuffled_str = '';
7570
7571 5
            foreach ($indexes as &$i) {
7572 5
                $tmp_sub_str = \mb_substr($str, $i, 1);
7573 5
                if ($tmp_sub_str !== false) {
7574 5
                    $shuffled_str .= $tmp_sub_str;
7575
                }
7576
            }
7577
        } else {
7578
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7579
7580
            $indexes = \range(0, (int) self::strlen($str, $encoding) - 1);
7581
            /** @noinspection NonSecureShuffleUsageInspection */
7582
            \shuffle($indexes);
7583
7584
            // init
7585
            $shuffled_str = '';
7586
7587
            foreach ($indexes as &$i) {
7588
                $tmp_sub_str = self::substr($str, $i, 1, $encoding);
7589
                if ($tmp_sub_str !== false) {
7590
                    $shuffled_str .= $tmp_sub_str;
7591
                }
7592
            }
7593
        }
7594
7595 5
        return $shuffled_str;
7596
    }
7597
7598
    /**
7599
     * Returns the substring beginning at $start, and up to, but not including
7600
     * the index specified by $end. If $end is omitted, the function extracts
7601
     * the remaining string. If $end is negative, it is computed from the end
7602
     * of the string.
7603
     *
7604
     * @param string $str
7605
     * @param int    $start    <p>Initial index from which to begin extraction.</p>
7606
     * @param int    $end      [optional] <p>Index at which to end extraction. Default: null</p>
7607
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7608
     *
7609
     * @psalm-pure
7610
     *
7611
     * @return false|string
7612
     *                      <p>The extracted substring.</p><p>If <i>str</i> is shorter than <i>start</i>
7613
     *                      characters long, <b>FALSE</b> will be returned.
7614
     */
7615 18
    public static function str_slice(
7616
        string $str,
7617
        int $start,
7618
        int $end = null,
7619
        string $encoding = 'UTF-8'
7620
    ) {
7621 18
        if ($encoding === 'UTF-8') {
7622 7
            if ($end === null) {
7623 1
                $length = (int) \mb_strlen($str);
7624 6
            } elseif ($end >= 0 && $end <= $start) {
7625 2
                return '';
7626 4
            } elseif ($end < 0) {
7627 1
                $length = (int) \mb_strlen($str) + $end - $start;
7628
            } else {
7629 3
                $length = $end - $start;
7630
            }
7631
7632 5
            return \mb_substr($str, $start, $length);
7633
        }
7634
7635 11
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7636
7637 11
        if ($end === null) {
7638 5
            $length = (int) self::strlen($str, $encoding);
7639 6
        } elseif ($end >= 0 && $end <= $start) {
7640 2
            return '';
7641 4
        } elseif ($end < 0) {
7642 1
            $length = (int) self::strlen($str, $encoding) + $end - $start;
7643
        } else {
7644 3
            $length = $end - $start;
7645
        }
7646
7647 9
        return self::substr($str, $start, $length, $encoding);
7648
    }
7649
7650
    /**
7651
     * Convert a string to e.g.: "snake_case"
7652
     *
7653
     * @param string $str
7654
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7655
     *
7656
     * @psalm-pure
7657
     *
7658
     * @return string
7659
     *                <p>A string in snake_case.</p>
7660
     */
7661 22
    public static function str_snakeize(string $str, string $encoding = 'UTF-8'): string
7662
    {
7663 22
        if ($str === '') {
7664
            return '';
7665
        }
7666
7667 22
        $str = \str_replace(
7668 22
            '-',
7669 22
            '_',
7670 22
            self::normalize_whitespace($str)
7671
        );
7672
7673 22
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
7674 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7675
        }
7676
7677 22
        $str = (string) \preg_replace_callback(
7678 22
            '/([\\p{N}|\\p{Lu}])/u',
7679
            /**
7680
             * @param string[] $matches
7681
             *
7682
             * @psalm-pure
7683
             *
7684
             * @return string
7685
             */
7686
            static function (array $matches) use ($encoding): string {
7687 9
                $match = $matches[1];
7688 9
                $match_int = (int) $match;
7689
7690 9
                if ((string) $match_int === $match) {
7691 4
                    return '_' . $match . '_';
7692
                }
7693
7694 5
                if ($encoding === 'UTF-8') {
7695 5
                    return '_' . \mb_strtolower($match);
7696
                }
7697
7698
                return '_' . self::strtolower($match, $encoding);
7699 22
            },
7700 22
            $str
7701
        );
7702
7703 22
        $str = (string) \preg_replace(
7704
            [
7705 22
                '/\\s+/u',           // convert spaces to "_"
7706
                '/^\\s+|\\s+$/u', // trim leading & trailing spaces
7707
                '/_+/',                 // remove double "_"
7708
            ],
7709
            [
7710 22
                '_',
7711
                '',
7712
                '_',
7713
            ],
7714 22
            $str
7715
        );
7716
7717 22
        return \trim(\trim($str, '_')); // trim leading & trailing "_" + whitespace
7718
    }
7719
7720
    /**
7721
     * Sort all characters according to code points.
7722
     *
7723
     * @param string $str    <p>A UTF-8 string.</p>
7724
     * @param bool   $unique <p>Sort unique. If <strong>true</strong>, repeated characters are ignored.</p>
7725
     * @param bool   $desc   <p>If <strong>true</strong>, will sort characters in reverse code point order.</p>
7726
     *
7727
     * @psalm-pure
7728
     *
7729
     * @return string
7730
     *                <p>A string of sorted characters.</p>
7731
     */
7732 2
    public static function str_sort(string $str, bool $unique = false, bool $desc = false): string
7733
    {
7734 2
        $array = self::codepoints($str);
7735
7736 2
        if ($unique) {
7737 2
            $array = \array_flip(\array_flip($array));
7738
        }
7739
7740 2
        if ($desc) {
7741 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

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

7743
            \asort(/** @scrutinizer ignore-type */ $array);
Loading history...
7744
        }
7745
7746 2
        return self::string($array);
7747
    }
7748
7749
    /**
7750
     * Convert a string to an array of Unicode characters.
7751
     *
7752
     * @param int|int[]|string|string[] $str                     <p>The string to split into array.</p>
7753
     * @param int                       $length                  [optional] <p>Max character length of each array
7754
     *                                                           element.</p>
7755
     * @param bool                      $clean_utf8              [optional] <p>Remove non UTF-8 chars from the
7756
     *                                                           string.</p>
7757
     * @param bool                      $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
7758
     *                                                           "mb_substr"</p>
7759
     *
7760
     * @psalm-pure
7761
     *
7762
     * @return array
7763
     *               <p>An array containing chunks of the input.</p>
7764
     */
7765 89
    public static function str_split(
7766
        $str,
7767
        int $length = 1,
7768
        bool $clean_utf8 = false,
7769
        bool $try_to_use_mb_functions = true
7770
    ): array {
7771 89
        if ($length <= 0) {
7772 3
            return [];
7773
        }
7774
7775 88
        if (\is_array($str) === true) {
7776 2
            foreach ($str as $k => &$v) {
7777 2
                $v = self::str_split(
7778 2
                    $v,
7779 2
                    $length,
7780 2
                    $clean_utf8,
7781 2
                    $try_to_use_mb_functions
7782
                );
7783
            }
7784
7785 2
            return $str;
7786
        }
7787
7788
        // init
7789 88
        $str = (string) $str;
7790
7791 88
        if ($str === '') {
7792 13
            return [];
7793
        }
7794
7795 85
        if ($clean_utf8 === true) {
7796 19
            $str = self::clean($str);
7797
        }
7798
7799
        if (
7800 85
            $try_to_use_mb_functions === true
7801
            &&
7802 85
            self::$SUPPORT['mbstring'] === true
7803
        ) {
7804 81
            if (Bootup::is_php('7.4')) {
7805
                /**
7806
                 * @psalm-suppress ImpureFunctionCall - why?
7807
                 */
7808
                $return = \mb_str_split($str, $length);
7809
                if ($return !== false) {
7810
                    return $return;
7811
                }
7812
            }
7813
7814 81
            $i_max = \mb_strlen($str);
7815 81
            if ($i_max <= 127) {
7816 75
                $ret = [];
7817 75
                for ($i = 0; $i < $i_max; ++$i) {
7818 75
                    $ret[] = \mb_substr($str, $i, 1);
7819
                }
7820
            } else {
7821 16
                $return_array = [];
7822 16
                \preg_match_all('/./us', $str, $return_array);
7823 81
                $ret = $return_array[0] ?? [];
7824
            }
7825 23
        } elseif (self::$SUPPORT['pcre_utf8'] === true) {
7826 17
            $return_array = [];
7827 17
            \preg_match_all('/./us', $str, $return_array);
7828 17
            $ret = $return_array[0] ?? [];
7829
        } else {
7830
7831
            // fallback
7832
7833 8
            $ret = [];
7834 8
            $len = \strlen($str);
7835
7836
            /** @noinspection ForeachInvariantsInspection */
7837 8
            for ($i = 0; $i < $len; ++$i) {
7838 8
                if (($str[$i] & "\x80") === "\x00") {
7839 8
                    $ret[] = $str[$i];
7840
                } elseif (
7841 8
                    isset($str[$i + 1])
7842
                    &&
7843 8
                    ($str[$i] & "\xE0") === "\xC0"
7844
                ) {
7845 4
                    if (($str[$i + 1] & "\xC0") === "\x80") {
7846 4
                        $ret[] = $str[$i] . $str[$i + 1];
7847
7848 4
                        ++$i;
7849
                    }
7850
                } elseif (
7851 6
                    isset($str[$i + 2])
7852
                    &&
7853 6
                    ($str[$i] & "\xF0") === "\xE0"
7854
                ) {
7855
                    if (
7856 6
                        ($str[$i + 1] & "\xC0") === "\x80"
7857
                        &&
7858 6
                        ($str[$i + 2] & "\xC0") === "\x80"
7859
                    ) {
7860 6
                        $ret[] = $str[$i] . $str[$i + 1] . $str[$i + 2];
7861
7862 6
                        $i += 2;
7863
                    }
7864
                } elseif (
7865
                    isset($str[$i + 3])
7866
                    &&
7867
                    ($str[$i] & "\xF8") === "\xF0"
7868
                ) {
7869
                    if (
7870
                        ($str[$i + 1] & "\xC0") === "\x80"
7871
                        &&
7872
                        ($str[$i + 2] & "\xC0") === "\x80"
7873
                        &&
7874
                        ($str[$i + 3] & "\xC0") === "\x80"
7875
                    ) {
7876
                        $ret[] = $str[$i] . $str[$i + 1] . $str[$i + 2] . $str[$i + 3];
7877
7878
                        $i += 3;
7879
                    }
7880
                }
7881
            }
7882
        }
7883
7884 85
        if ($length > 1) {
7885 11
            $ret = \array_chunk($ret, $length);
7886
7887 11
            return \array_map(
7888
                static function (array &$item): string {
7889 11
                    return \implode('', $item);
7890 11
                },
7891 11
                $ret
7892
            );
7893
        }
7894
7895 78
        if (isset($ret[0]) && $ret[0] === '') {
7896
            return [];
7897
        }
7898
7899 78
        return $ret;
7900
    }
7901
7902
    /**
7903
     * Splits the string with the provided regular expression, returning an
7904
     * array of strings. An optional integer $limit will truncate the
7905
     * results.
7906
     *
7907
     * @param string $str
7908
     * @param string $pattern <p>The regex with which to split the string.</p>
7909
     * @param int    $limit   [optional] <p>Maximum number of results to return. Default: -1 === no limit</p>
7910
     *
7911
     * @psalm-pure
7912
     *
7913
     * @return string[]
7914
     *                  <p>An array of strings.</p>
7915
     */
7916 16
    public static function str_split_pattern(string $str, string $pattern, int $limit = -1): array
7917
    {
7918 16
        if ($limit === 0) {
7919 2
            return [];
7920
        }
7921
7922 14
        if ($pattern === '') {
7923 1
            return [$str];
7924
        }
7925
7926 13
        if (self::$SUPPORT['mbstring'] === true) {
7927 13
            if ($limit >= 0) {
7928
                /** @noinspection PhpComposerExtensionStubsInspection */
7929 8
                $result_tmp = \mb_split($pattern, $str);
7930
7931 8
                $result = [];
7932 8
                foreach ($result_tmp as $item_tmp) {
7933 8
                    if ($limit === 0) {
7934 4
                        break;
7935
                    }
7936 8
                    --$limit;
7937
7938 8
                    $result[] = $item_tmp;
7939
                }
7940
7941 8
                return $result;
7942
            }
7943
7944
            /** @noinspection PhpComposerExtensionStubsInspection */
7945 5
            return \mb_split($pattern, $str);
7946
        }
7947
7948
        if ($limit > 0) {
7949
            ++$limit;
7950
        } else {
7951
            $limit = -1;
7952
        }
7953
7954
        $array = \preg_split('/' . \preg_quote($pattern, '/') . '/u', $str, $limit);
7955
7956
        if ($array === false) {
7957
            return [];
7958
        }
7959
7960
        if ($limit > 0 && \count($array) === $limit) {
7961
            \array_pop($array);
7962
        }
7963
7964
        return $array;
7965
    }
7966
7967
    /**
7968
     * Check if the string starts with the given substring.
7969
     *
7970
     * @param string $haystack <p>The string to search in.</p>
7971
     * @param string $needle   <p>The substring to search for.</p>
7972
     *
7973
     * @psalm-pure
7974
     *
7975
     * @return bool
7976
     */
7977 19
    public static function str_starts_with(string $haystack, string $needle): bool
7978
    {
7979 19
        if ($needle === '') {
7980 2
            return true;
7981
        }
7982
7983 19
        if ($haystack === '') {
7984
            return false;
7985
        }
7986
7987 19
        return \strpos($haystack, $needle) === 0;
7988
    }
7989
7990
    /**
7991
     * Returns true if the string begins with any of $substrings, false otherwise.
7992
     *
7993
     * - case-sensitive
7994
     *
7995
     * @param string $str        <p>The input string.</p>
7996
     * @param array  $substrings <p>Substrings to look for.</p>
7997
     *
7998
     * @psalm-pure
7999
     *
8000
     * @return bool whether or not $str starts with $substring
8001
     */
8002 8
    public static function str_starts_with_any(string $str, array $substrings): bool
8003
    {
8004 8
        if ($str === '') {
8005
            return false;
8006
        }
8007
8008 8
        if ($substrings === []) {
8009
            return false;
8010
        }
8011
8012 8
        foreach ($substrings as &$substring) {
8013 8
            if (self::str_starts_with($str, $substring)) {
8014 8
                return true;
8015
            }
8016
        }
8017
8018 6
        return false;
8019
    }
8020
8021
    /**
8022
     * Gets the substring after the first occurrence of a separator.
8023
     *
8024
     * @param string $str       <p>The input string.</p>
8025
     * @param string $separator <p>The string separator.</p>
8026
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8027
     *
8028
     * @psalm-pure
8029
     *
8030
     * @return string
8031
     */
8032 1
    public static function str_substr_after_first_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
8033
    {
8034 1
        if ($separator === '' || $str === '') {
8035 1
            return '';
8036
        }
8037
8038 1
        if ($encoding === 'UTF-8') {
8039 1
            $offset = \mb_strpos($str, $separator);
8040 1
            if ($offset === false) {
8041 1
                return '';
8042
            }
8043
8044 1
            return (string) \mb_substr(
8045 1
                $str,
8046 1
                $offset + (int) \mb_strlen($separator)
8047
            );
8048
        }
8049
8050
        $offset = self::strpos($str, $separator, 0, $encoding);
8051
        if ($offset === false) {
8052
            return '';
8053
        }
8054
8055
        return (string) \mb_substr(
8056
            $str,
8057
            $offset + (int) self::strlen($separator, $encoding),
8058
            null,
8059
            $encoding
8060
        );
8061
    }
8062
8063
    /**
8064
     * Gets the substring after the last occurrence of a separator.
8065
     *
8066
     * @param string $str       <p>The input string.</p>
8067
     * @param string $separator <p>The string separator.</p>
8068
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8069
     *
8070
     * @psalm-pure
8071
     *
8072
     * @return string
8073
     */
8074 1
    public static function str_substr_after_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
8075
    {
8076 1
        if ($separator === '' || $str === '') {
8077 1
            return '';
8078
        }
8079
8080 1
        if ($encoding === 'UTF-8') {
8081 1
            $offset = \mb_strrpos($str, $separator);
8082 1
            if ($offset === false) {
8083 1
                return '';
8084
            }
8085
8086 1
            return (string) \mb_substr(
8087 1
                $str,
8088 1
                $offset + (int) \mb_strlen($separator)
8089
            );
8090
        }
8091
8092
        $offset = self::strrpos($str, $separator, 0, $encoding);
8093
        if ($offset === false) {
8094
            return '';
8095
        }
8096
8097
        return (string) self::substr(
8098
            $str,
8099
            $offset + (int) self::strlen($separator, $encoding),
8100
            null,
8101
            $encoding
8102
        );
8103
    }
8104
8105
    /**
8106
     * Gets the substring before the first occurrence of a separator.
8107
     *
8108
     * @param string $str       <p>The input string.</p>
8109
     * @param string $separator <p>The string separator.</p>
8110
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8111
     *
8112
     * @psalm-pure
8113
     *
8114
     * @return string
8115
     */
8116 1
    public static function str_substr_before_first_separator(
8117
        string $str,
8118
        string $separator,
8119
        string $encoding = 'UTF-8'
8120
    ): string {
8121 1
        if ($separator === '' || $str === '') {
8122 1
            return '';
8123
        }
8124
8125 1
        if ($encoding === 'UTF-8') {
8126 1
            $offset = \mb_strpos($str, $separator);
8127 1
            if ($offset === false) {
8128 1
                return '';
8129
            }
8130
8131 1
            return (string) \mb_substr(
8132 1
                $str,
8133 1
                0,
8134 1
                $offset
8135
            );
8136
        }
8137
8138
        $offset = self::strpos($str, $separator, 0, $encoding);
8139
        if ($offset === false) {
8140
            return '';
8141
        }
8142
8143
        return (string) self::substr(
8144
            $str,
8145
            0,
8146
            $offset,
8147
            $encoding
8148
        );
8149
    }
8150
8151
    /**
8152
     * Gets the substring before the last occurrence of a separator.
8153
     *
8154
     * @param string $str       <p>The input string.</p>
8155
     * @param string $separator <p>The string separator.</p>
8156
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8157
     *
8158
     * @psalm-pure
8159
     *
8160
     * @return string
8161
     */
8162 1
    public static function str_substr_before_last_separator(string $str, string $separator, string $encoding = 'UTF-8'): string
8163
    {
8164 1
        if ($separator === '' || $str === '') {
8165 1
            return '';
8166
        }
8167
8168 1
        if ($encoding === 'UTF-8') {
8169 1
            $offset = \mb_strrpos($str, $separator);
8170 1
            if ($offset === false) {
8171 1
                return '';
8172
            }
8173
8174 1
            return (string) \mb_substr(
8175 1
                $str,
8176 1
                0,
8177 1
                $offset
8178
            );
8179
        }
8180
8181
        $offset = self::strrpos($str, $separator, 0, $encoding);
8182
        if ($offset === false) {
8183
            return '';
8184
        }
8185
8186
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8187
8188
        return (string) self::substr(
8189
            $str,
8190
            0,
8191
            $offset,
8192
            $encoding
8193
        );
8194
    }
8195
8196
    /**
8197
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
8198
     *
8199
     * @param string $str           <p>The input string.</p>
8200
     * @param string $needle        <p>The string to look for.</p>
8201
     * @param bool   $before_needle [optional] <p>Default: false</p>
8202
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
8203
     *
8204
     * @psalm-pure
8205
     *
8206
     * @return string
8207
     */
8208 2
    public static function str_substr_first(
8209
        string $str,
8210
        string $needle,
8211
        bool $before_needle = false,
8212
        string $encoding = 'UTF-8'
8213
    ): string {
8214 2
        if ($str === '' || $needle === '') {
8215 2
            return '';
8216
        }
8217
8218 2
        if ($encoding === 'UTF-8') {
8219 2
            if ($before_needle === true) {
8220 1
                $part = \mb_strstr(
8221 1
                    $str,
8222 1
                    $needle,
8223 1
                    $before_needle
8224
                );
8225
            } else {
8226 1
                $part = \mb_strstr(
8227 1
                    $str,
8228 2
                    $needle
8229
                );
8230
            }
8231
        } else {
8232
            $part = self::strstr(
8233
                $str,
8234
                $needle,
8235
                $before_needle,
8236
                $encoding
8237
            );
8238
        }
8239
8240 2
        return $part === false ? '' : $part;
8241
    }
8242
8243
    /**
8244
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
8245
     *
8246
     * @param string $str           <p>The input string.</p>
8247
     * @param string $needle        <p>The string to look for.</p>
8248
     * @param bool   $before_needle [optional] <p>Default: false</p>
8249
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
8250
     *
8251
     * @psalm-pure
8252
     *
8253
     * @return string
8254
     */
8255 2
    public static function str_substr_last(
8256
        string $str,
8257
        string $needle,
8258
        bool $before_needle = false,
8259
        string $encoding = 'UTF-8'
8260
    ): string {
8261 2
        if ($str === '' || $needle === '') {
8262 2
            return '';
8263
        }
8264
8265 2
        if ($encoding === 'UTF-8') {
8266 2
            if ($before_needle === true) {
8267 1
                $part = \mb_strrchr(
8268 1
                    $str,
8269 1
                    $needle,
8270 1
                    $before_needle
8271
                );
8272
            } else {
8273 1
                $part = \mb_strrchr(
8274 1
                    $str,
8275 2
                    $needle
8276
                );
8277
            }
8278
        } else {
8279
            $part = self::strrchr(
8280
                $str,
8281
                $needle,
8282
                $before_needle,
8283
                $encoding
8284
            );
8285
        }
8286
8287 2
        return $part === false ? '' : $part;
8288
    }
8289
8290
    /**
8291
     * Surrounds $str with the given substring.
8292
     *
8293
     * @param string $str
8294
     * @param string $substring <p>The substring to add to both sides.</P>
8295
     *
8296
     * @psalm-pure
8297
     *
8298
     * @return string
8299
     *                <p>A string with the substring both prepended and appended.</p>
8300
     */
8301 5
    public static function str_surround(string $str, string $substring): string
8302
    {
8303 5
        return $substring . $str . $substring;
8304
    }
8305
8306
    /**
8307
     * Returns a trimmed string with the first letter of each word capitalized.
8308
     * Also accepts an array, $ignore, allowing you to list words not to be
8309
     * capitalized.
8310
     *
8311
     * @param string              $str
8312
     * @param array|string[]|null $ignore                        [optional] <p>An array of words not to capitalize or
8313
     *                                                           null. Default: null</p>
8314
     * @param string              $encoding                      [optional] <p>Default: 'UTF-8'</p>
8315
     * @param bool                $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the
8316
     *                                                           string.</p>
8317
     * @param string|null         $lang                          [optional] <p>Set the language for special cases: az,
8318
     *                                                           el, lt, tr</p>
8319
     * @param bool                $try_to_keep_the_string_length [optional] <p>true === try to keep the string length:
8320
     *                                                           e.g. ẞ -> ß</p>
8321
     * @param bool                $use_trim_first                [optional] <p>true === trim the input string,
8322
     *                                                           first</p>
8323
     * @param string|null         $word_define_chars             [optional] <p>An string of chars that will be used as
8324
     *                                                           whitespace separator === words.</p>
8325
     *
8326
     * @psalm-pure
8327
     *
8328
     * @return string
8329
     *                <p>The titleized string.</p>
8330
     */
8331 10
    public static function str_titleize(
8332
        string $str,
8333
        array $ignore = null,
8334
        string $encoding = 'UTF-8',
8335
        bool $clean_utf8 = false,
8336
        string $lang = null,
8337
        bool $try_to_keep_the_string_length = false,
8338
        bool $use_trim_first = true,
8339
        string $word_define_chars = null
8340
    ): string {
8341 10
        if ($str === '') {
8342
            return '';
8343
        }
8344
8345 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
8346 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8347
        }
8348
8349 10
        if ($use_trim_first === true) {
8350 10
            $str = \trim($str);
8351
        }
8352
8353 10
        if ($clean_utf8 === true) {
8354
            $str = self::clean($str);
8355
        }
8356
8357 10
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
8358
8359 10
        if ($word_define_chars) {
8360 4
            $word_define_chars = \preg_quote($word_define_chars, '/');
8361
        } else {
8362 6
            $word_define_chars = '';
8363
        }
8364
8365 10
        $str = (string) \preg_replace_callback(
8366 10
            '/([^\\s' . $word_define_chars . ']+)/u',
8367
            static function (array $match) use ($try_to_keep_the_string_length, $lang, $ignore, $use_mb_functions, $encoding): string {
8368 10
                if ($ignore !== null && \in_array($match[0], $ignore, true)) {
8369 4
                    return $match[0];
8370
                }
8371
8372 10
                if ($use_mb_functions === true) {
8373 10
                    if ($encoding === 'UTF-8') {
8374 10
                        return \mb_strtoupper(\mb_substr($match[0], 0, 1))
8375 10
                               . \mb_strtolower(\mb_substr($match[0], 1));
8376
                    }
8377
8378
                    return \mb_strtoupper(\mb_substr($match[0], 0, 1, $encoding), $encoding)
8379
                           . \mb_strtolower(\mb_substr($match[0], 1, null, $encoding), $encoding);
8380
                }
8381
8382
                return self::ucfirst(
8383
                    self::strtolower(
8384
                        $match[0],
8385
                        $encoding,
8386
                        false,
8387
                        $lang,
8388
                        $try_to_keep_the_string_length
8389
                    ),
8390
                    $encoding,
8391
                    false,
8392
                    $lang,
8393
                    $try_to_keep_the_string_length
8394
                );
8395 10
            },
8396 10
            $str
8397
        );
8398
8399 10
        return $str;
8400
    }
8401
8402
    /**
8403
     * Returns a trimmed string in proper title case.
8404
     *
8405
     * Also accepts an array, $ignore, allowing you to list words not to be
8406
     * capitalized.
8407
     *
8408
     * Adapted from John Gruber's script.
8409
     *
8410
     * @see https://gist.github.com/gruber/9f9e8650d68b13ce4d78
8411
     *
8412
     * @param string $str
8413
     * @param array  $ignore   <p>An array of words not to capitalize.</p>
8414
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
8415
     *
8416
     * @psalm-pure
8417
     *
8418
     * @return string
8419
     *                <p>The titleized string.</p>
8420
     */
8421 35
    public static function str_titleize_for_humans(
8422
        string $str,
8423
        array $ignore = [],
8424
        string $encoding = 'UTF-8'
8425
    ): string {
8426 35
        if ($str === '') {
8427
            return '';
8428
        }
8429
8430
        $small_words = [
8431 35
            '(?<!q&)a',
8432
            'an',
8433
            'and',
8434
            'as',
8435
            'at(?!&t)',
8436
            'but',
8437
            'by',
8438
            'en',
8439
            'for',
8440
            'if',
8441
            'in',
8442
            'of',
8443
            'on',
8444
            'or',
8445
            'the',
8446
            'to',
8447
            'v[.]?',
8448
            'via',
8449
            'vs[.]?',
8450
        ];
8451
8452 35
        if ($ignore !== []) {
8453 1
            $small_words = \array_merge($small_words, $ignore);
8454
        }
8455
8456 35
        $small_words_rx = \implode('|', $small_words);
8457 35
        $apostrophe_rx = '(?x: [\'’] [[:lower:]]* )?';
8458
8459 35
        $str = \trim($str);
8460
8461 35
        if (self::has_lowercase($str) === false) {
8462 2
            $str = self::strtolower($str, $encoding);
8463
        }
8464
8465
        // the main substitutions
8466 35
        $str = (string) \preg_replace_callback(
8467
            '~\\b (_*) (?:                                                         # 1. Leading underscore and
8468
                        ( (?<=[ ][/\\\\]) [[:alpha:]]+ [-_[:alpha:]/\\\\]+ |              # 2. file path or 
8469 35
                          [-_[:alpha:]]+ [@.:] [-_[:alpha:]@.:/]+ ' . $apostrophe_rx . ' ) #    URL, domain, or email
8470
                        |
8471 35
                        ( (?i: ' . $small_words_rx . ' ) ' . $apostrophe_rx . ' )            # 3. or small word (case-insensitive)
8472
                        |
8473 35
                        ( [[:alpha:]] [[:lower:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 4. or word w/o internal caps
8474
                        |
8475 35
                        ( [[:alpha:]] [[:alpha:]\'’()\[\]{}]* ' . $apostrophe_rx . ' )     # 5. or some other word
8476
                      ) (_*) \\b                                                          # 6. With trailing underscore
8477
                    ~ux',
8478
            /**
8479
             * @param string[] $matches
8480
             *
8481
             * @psalm-pure
8482
             *
8483
             * @return string
8484
             */
8485
            static function (array $matches) use ($encoding): string {
8486
                // preserve leading underscore
8487 35
                $str = $matches[1];
8488 35
                if ($matches[2]) {
8489
                    // preserve URLs, domains, emails and file paths
8490 5
                    $str .= $matches[2];
8491 35
                } elseif ($matches[3]) {
8492
                    // lower-case small words
8493 25
                    $str .= self::strtolower($matches[3], $encoding);
8494 35
                } elseif ($matches[4]) {
8495
                    // capitalize word w/o internal caps
8496 34
                    $str .= static::ucfirst($matches[4], $encoding);
8497
                } else {
8498
                    // preserve other kinds of word (iPhone)
8499 7
                    $str .= $matches[5];
8500
                }
8501
                // preserve trailing underscore
8502 35
                $str .= $matches[6];
8503
8504 35
                return $str;
8505 35
            },
8506 35
            $str
8507
        );
8508
8509
        // Exceptions for small words: capitalize at start of title...
8510 35
        $str = (string) \preg_replace_callback(
8511
            '~(  \\A [[:punct:]]*            # start of title...
8512
                      |  [:.;?!][ ]+                # or of subsentence...
8513
                      |  [ ][\'"“‘(\[][ ]* )        # or of inserted subphrase...
8514 35
                      ( ' . $small_words_rx . ' ) \\b # ...followed by small word
8515
                     ~uxi',
8516
            /**
8517
             * @param string[] $matches
8518
             *
8519
             * @psalm-pure
8520
             *
8521
             * @return string
8522
             */
8523
            static function (array $matches) use ($encoding): string {
8524 11
                return $matches[1] . static::ucfirst($matches[2], $encoding);
8525 35
            },
8526 35
            $str
8527
        );
8528
8529
        // ...and end of title
8530 35
        $str = (string) \preg_replace_callback(
8531 35
            '~\\b ( ' . $small_words_rx . ' ) # small word...
8532
                      (?= [[:punct:]]* \Z          # ...at the end of the title...
8533
                      |   [\'"’”)\]] [ ] )         # ...or of an inserted subphrase?
8534
                     ~uxi',
8535
            /**
8536
             * @param string[] $matches
8537
             *
8538
             * @psalm-pure
8539
             *
8540
             * @return string
8541
             */
8542
            static function (array $matches) use ($encoding): string {
8543 3
                return static::ucfirst($matches[1], $encoding);
8544 35
            },
8545 35
            $str
8546
        );
8547
8548
        // Exceptions for small words in hyphenated compound words.
8549
        // e.g. "in-flight" -> In-Flight
8550 35
        $str = (string) \preg_replace_callback(
8551
            '~\\b
8552
                        (?<! -)                   # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (in-flight)
8553 35
                        ( ' . $small_words_rx . ' )
8554
                        (?= -[[:alpha:]]+)        # lookahead for "-someword"
8555
                       ~uxi',
8556
            /**
8557
             * @param string[] $matches
8558
             *
8559
             * @psalm-pure
8560
             *
8561
             * @return string
8562
             */
8563
            static function (array $matches) use ($encoding): string {
8564
                return static::ucfirst($matches[1], $encoding);
8565 35
            },
8566 35
            $str
8567
        );
8568
8569
        // e.g. "Stand-in" -> "Stand-In" (Stand is already capped at this point)
8570 35
        $str = (string) \preg_replace_callback(
8571
            '~\\b
8572
                      (?<!…)                    # Negative lookbehind for a hyphen; we do not want to match man-in-the-middle but do want (stand-in)
8573
                      ( [[:alpha:]]+- )         # $1 = first word and hyphen, should already be properly capped
8574 35
                      ( ' . $small_words_rx . ' ) # ...followed by small word
8575
                      (?!	- )                 # Negative lookahead for another -
8576
                     ~uxi',
8577
            /**
8578
             * @param string[] $matches
8579
             *
8580
             * @psalm-pure
8581
             *
8582
             * @return string
8583
             */
8584
            static function (array $matches) use ($encoding): string {
8585
                return $matches[1] . static::ucfirst($matches[2], $encoding);
8586 35
            },
8587 35
            $str
8588
        );
8589
8590 35
        return $str;
8591
    }
8592
8593
    /**
8594
     * Get a binary representation of a specific string.
8595
     *
8596
     * @param string $str <p>The input string.</p>
8597
     *
8598
     * @psalm-pure
8599
     *
8600
     * @return false|string
8601
     *                      <p>false on error</p>
8602
     */
8603 2
    public static function str_to_binary(string $str)
8604
    {
8605
        /** @var array|false $value - needed for PhpStan (stubs error) */
8606 2
        $value = \unpack('H*', $str);
8607 2
        if ($value === false) {
8608
            return false;
8609
        }
8610
8611
        /** @noinspection OffsetOperationsInspection */
8612 2
        return \base_convert($value[1], 16, 2);
8613
    }
8614
8615
    /**
8616
     * @param string   $str
8617
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
8618
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
8619
     *
8620
     * @psalm-pure
8621
     *
8622
     * @return string[]
8623
     */
8624 17
    public static function str_to_lines(string $str, bool $remove_empty_values = false, int $remove_short_values = null): array
8625
    {
8626 17
        if ($str === '') {
8627 1
            return $remove_empty_values === true ? [] : [''];
8628
        }
8629
8630 16
        if (self::$SUPPORT['mbstring'] === true) {
8631
            /** @noinspection PhpComposerExtensionStubsInspection */
8632 16
            $return = \mb_split("[\r\n]{1,2}", $str);
8633
        } else {
8634
            $return = \preg_split("/[\r\n]{1,2}/u", $str);
8635
        }
8636
8637 16
        if ($return === false) {
8638
            return $remove_empty_values === true ? [] : [''];
8639
        }
8640
8641
        if (
8642 16
            $remove_short_values === null
8643
            &&
8644 16
            $remove_empty_values === false
8645
        ) {
8646 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...
8647
        }
8648
8649
        return self::reduce_string_array(
8650
            $return,
8651
            $remove_empty_values,
8652
            $remove_short_values
8653
        );
8654
    }
8655
8656
    /**
8657
     * Convert a string into an array of words.
8658
     *
8659
     * @param string   $str
8660
     * @param string   $char_list           <p>Additional chars for the definition of "words".</p>
8661
     * @param bool     $remove_empty_values <p>Remove empty values.</p>
8662
     * @param int|null $remove_short_values <p>The min. string length or null to disable</p>
8663
     *
8664
     * @psalm-pure
8665
     *
8666
     * @return string[]
8667
     */
8668 13
    public static function str_to_words(
8669
        string $str,
8670
        string $char_list = '',
8671
        bool $remove_empty_values = false,
8672
        int $remove_short_values = null
8673
    ): array {
8674 13
        if ($str === '') {
8675 4
            return $remove_empty_values === true ? [] : [''];
8676
        }
8677
8678 13
        $char_list = self::rxClass($char_list, '\pL');
8679
8680 13
        $return = \preg_split("/({$char_list}+(?:[\p{Pd}’']{$char_list}+)*)/u", $str, -1, \PREG_SPLIT_DELIM_CAPTURE);
8681 13
        if ($return === false) {
8682
            return $remove_empty_values === true ? [] : [''];
8683
        }
8684
8685
        if (
8686 13
            $remove_short_values === null
8687
            &&
8688 13
            $remove_empty_values === false
8689
        ) {
8690 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...
8691
        }
8692
8693 2
        $tmp_return = self::reduce_string_array(
8694 2
            $return,
8695 2
            $remove_empty_values,
8696 2
            $remove_short_values
8697
        );
8698
8699 2
        foreach ($tmp_return as &$item) {
8700 2
            $item = (string) $item;
8701
        }
8702
8703 2
        return $tmp_return;
8704
    }
8705
8706
    /**
8707
     * alias for "UTF8::to_ascii()"
8708
     *
8709
     * @param string $str
8710
     * @param string $unknown
8711
     * @param bool   $strict
8712
     *
8713
     * @psalm-pure
8714
     *
8715
     * @return string
8716
     *
8717
     * @see        UTF8::to_ascii()
8718
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
8719
     */
8720 7
    public static function str_transliterate(
8721
        string $str,
8722
        string $unknown = '?',
8723
        bool $strict = false
8724
    ): string {
8725 7
        return self::to_ascii($str, $unknown, $strict);
8726
    }
8727
8728
    /**
8729
     * Truncates the string to a given length. If $substring is provided, and
8730
     * truncating occurs, the string is further truncated so that the substring
8731
     * may be appended without exceeding the desired length.
8732
     *
8733
     * @param string $str
8734
     * @param int    $length    <p>Desired length of the truncated string.</p>
8735
     * @param string $substring [optional] <p>The substring to append if it can fit. Default: ''</p>
8736
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
8737
     *
8738
     * @psalm-pure
8739
     *
8740
     * @return string
8741
     *                <p>A string after truncating.</p>
8742
     */
8743 22
    public static function str_truncate(
8744
        string $str,
8745
        int $length,
8746
        string $substring = '',
8747
        string $encoding = 'UTF-8'
8748
    ): string {
8749 22
        if ($str === '') {
8750
            return '';
8751
        }
8752
8753 22
        if ($encoding === 'UTF-8') {
8754 10
            if ($length >= (int) \mb_strlen($str)) {
8755 2
                return $str;
8756
            }
8757
8758 8
            if ($substring !== '') {
8759 4
                $length -= (int) \mb_strlen($substring);
8760
8761
                /** @noinspection UnnecessaryCastingInspection */
8762 4
                return (string) \mb_substr($str, 0, $length) . $substring;
8763
            }
8764
8765
            /** @noinspection UnnecessaryCastingInspection */
8766 4
            return (string) \mb_substr($str, 0, $length);
8767
        }
8768
8769 12
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
8770
8771 12
        if ($length >= (int) self::strlen($str, $encoding)) {
8772 2
            return $str;
8773
        }
8774
8775 10
        if ($substring !== '') {
8776 6
            $length -= (int) self::strlen($substring, $encoding);
8777
        }
8778
8779
        return (
8780 10
               (string) self::substr(
8781 10
                   $str,
8782 10
                   0,
8783 10
                   $length,
8784 10
                   $encoding
8785
               )
8786 10
               ) . $substring;
8787
    }
8788
8789
    /**
8790
     * Truncates the string to a given length, while ensuring that it does not
8791
     * split words. If $substring is provided, and truncating occurs, the
8792
     * string is further truncated so that the substring may be appended without
8793
     * exceeding the desired length.
8794
     *
8795
     * @param string $str
8796
     * @param int    $length                                 <p>Desired length of the truncated string.</p>
8797
     * @param string $substring                              [optional] <p>The substring to append if it can fit.
8798
     *                                                       Default:
8799
     *                                                       ''</p>
8800
     * @param string $encoding                               [optional] <p>Default: 'UTF-8'</p>
8801
     * @param bool   $ignore_do_not_split_words_for_one_word [optional] <p>Default: false</p>
8802
     *
8803
     * @psalm-pure
8804
     *
8805
     * @return string
8806
     *                <p>A string after truncating.</p>
8807
     */
8808 47
    public static function str_truncate_safe(
8809
        string $str,
8810
        int $length,
8811
        string $substring = '',
8812
        string $encoding = 'UTF-8',
8813
        bool $ignore_do_not_split_words_for_one_word = false
8814
    ): string {
8815 47
        if ($str === '' || $length <= 0) {
8816 1
            return $substring;
8817
        }
8818
8819 47
        if ($encoding === 'UTF-8') {
8820 21
            if ($length >= (int) \mb_strlen($str)) {
8821 5
                return $str;
8822
            }
8823
8824
            // need to further trim the string so we can append the substring
8825 17
            $length -= (int) \mb_strlen($substring);
8826 17
            if ($length <= 0) {
8827 1
                return $substring;
8828
            }
8829
8830
            /** @var false|string $truncated - needed for PhpStan (stubs error) */
8831 17
            $truncated = \mb_substr($str, 0, $length);
8832 17
            if ($truncated === false) {
8833
                return '';
8834
            }
8835
8836
            // if the last word was truncated
8837 17
            $space_position = \mb_strpos($str, ' ', $length - 1);
8838 17
            if ($space_position !== $length) {
8839
                // find pos of the last occurrence of a space, get up to that
8840 13
                $last_position = \mb_strrpos($truncated, ' ', 0);
8841
8842
                if (
8843 13
                    $last_position !== false
8844
                    ||
8845 13
                    ($space_position !== false && $ignore_do_not_split_words_for_one_word === false)
8846
                ) {
8847 17
                    $truncated = (string) \mb_substr($truncated, 0, (int) $last_position);
8848
                }
8849
            }
8850
        } else {
8851 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
8852
8853 26
            if ($length >= (int) self::strlen($str, $encoding)) {
8854 4
                return $str;
8855
            }
8856
8857
            // need to further trim the string so we can append the substring
8858 22
            $length -= (int) self::strlen($substring, $encoding);
8859 22
            if ($length <= 0) {
8860
                return $substring;
8861
            }
8862
8863 22
            $truncated = self::substr($str, 0, $length, $encoding);
8864
8865 22
            if ($truncated === false) {
8866
                return '';
8867
            }
8868
8869
            // if the last word was truncated
8870 22
            $space_position = self::strpos($str, ' ', $length - 1, $encoding);
8871 22
            if ($space_position !== $length) {
8872
                // find pos of the last occurrence of a space, get up to that
8873 12
                $last_position = self::strrpos($truncated, ' ', 0, $encoding);
8874
8875
                if (
8876 12
                    $last_position !== false
8877
                    ||
8878 12
                    ($space_position !== false && $ignore_do_not_split_words_for_one_word === false)
8879
                ) {
8880 9
                    $truncated = (string) self::substr($truncated, 0, (int) $last_position, $encoding);
8881
                }
8882
            }
8883
        }
8884
8885 39
        return $truncated . $substring;
8886
    }
8887
8888
    /**
8889
     * Returns a lowercase and trimmed string separated by underscores.
8890
     * Underscores are inserted before uppercase characters (with the exception
8891
     * of the first character of the string), and in place of spaces as well as
8892
     * dashes.
8893
     *
8894
     * @param string $str
8895
     *
8896
     * @psalm-pure
8897
     *
8898
     * @return string
8899
     *                <p>The underscored string.</p>
8900
     */
8901 16
    public static function str_underscored(string $str): string
8902
    {
8903 16
        return self::str_delimit($str, '_');
8904
    }
8905
8906
    /**
8907
     * Returns an UpperCamelCase version of the supplied string. It trims
8908
     * surrounding spaces, capitalizes letters following digits, spaces, dashes
8909
     * and underscores, and removes spaces, dashes, underscores.
8910
     *
8911
     * @param string      $str                           <p>The input string.</p>
8912
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
8913
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
8914
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
8915
     *                                                   tr</p>
8916
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
8917
     *                                                   -> ß</p>
8918
     *
8919
     * @psalm-pure
8920
     *
8921
     * @return string
8922
     *                <p>A string in UpperCamelCase.</p>
8923
     */
8924 13
    public static function str_upper_camelize(
8925
        string $str,
8926
        string $encoding = 'UTF-8',
8927
        bool $clean_utf8 = false,
8928
        string $lang = null,
8929
        bool $try_to_keep_the_string_length = false
8930
    ): string {
8931 13
        return self::ucfirst(self::str_camelize($str, $encoding), $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
8932
    }
8933
8934
    /**
8935
     * alias for "UTF8::ucfirst()"
8936
     *
8937
     * @param string      $str
8938
     * @param string      $encoding
8939
     * @param bool        $clean_utf8
8940
     * @param string|null $lang
8941
     * @param bool        $try_to_keep_the_string_length
8942
     *
8943
     * @psalm-pure
8944
     *
8945
     * @return string
8946
     *
8947
     * @see        UTF8::ucfirst()
8948
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
8949
     */
8950 5
    public static function str_upper_first(
8951
        string $str,
8952
        string $encoding = 'UTF-8',
8953
        bool $clean_utf8 = false,
8954
        string $lang = null,
8955
        bool $try_to_keep_the_string_length = false
8956
    ): string {
8957 5
        return self::ucfirst(
8958 5
            $str,
8959 5
            $encoding,
8960 5
            $clean_utf8,
8961 5
            $lang,
8962 5
            $try_to_keep_the_string_length
8963
        );
8964
    }
8965
8966
    /**
8967
     * Get the number of words in a specific string.
8968
     *
8969
     * @param string $str       <p>The input string.</p>
8970
     * @param int    $format    [optional] <p>
8971
     *                          <strong>0</strong> => return a number of words (default)<br>
8972
     *                          <strong>1</strong> => return an array of words<br>
8973
     *                          <strong>2</strong> => return an array of words with word-offset as key
8974
     *                          </p>
8975
     * @param string $char_list [optional] <p>Additional chars that contains to words and do not start a new word.</p>
8976
     *
8977
     * @psalm-pure
8978
     *
8979
     * @return int|string[] The number of words in the string
8980
     */
8981 2
    public static function str_word_count(string $str, int $format = 0, string $char_list = '')
8982
    {
8983 2
        $str_parts = self::str_to_words($str, $char_list);
8984
8985 2
        $len = \count($str_parts);
8986
8987 2
        if ($format === 1) {
8988 2
            $number_of_words = [];
8989 2
            for ($i = 1; $i < $len; $i += 2) {
8990 2
                $number_of_words[] = $str_parts[$i];
8991
            }
8992 2
        } elseif ($format === 2) {
8993 2
            $number_of_words = [];
8994 2
            $offset = (int) self::strlen($str_parts[0]);
8995 2
            for ($i = 1; $i < $len; $i += 2) {
8996 2
                $number_of_words[$offset] = $str_parts[$i];
8997 2
                $offset += (int) self::strlen($str_parts[$i]) + (int) self::strlen($str_parts[$i + 1]);
8998
            }
8999
        } else {
9000 2
            $number_of_words = (int) (($len - 1) / 2);
9001
        }
9002
9003 2
        return $number_of_words;
9004
    }
9005
9006
    /**
9007
     * Case-insensitive string comparison.
9008
     *
9009
     * INFO: Case-insensitive version of UTF8::strcmp()
9010
     *
9011
     * @param string $str1     <p>The first string.</p>
9012
     * @param string $str2     <p>The second string.</p>
9013
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9014
     *
9015
     * @psalm-pure
9016
     *
9017
     * @return int
9018
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
9019
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
9020
     *             <strong>0</strong> if they are equal
9021
     */
9022 23
    public static function strcasecmp(
9023
        string $str1,
9024
        string $str2,
9025
        string $encoding = 'UTF-8'
9026
    ): int {
9027 23
        return self::strcmp(
9028 23
            self::strtocasefold(
9029 23
                $str1,
9030 23
                true,
9031 23
                false,
9032 23
                $encoding,
9033 23
                null,
9034 23
                false
9035
            ),
9036 23
            self::strtocasefold(
9037 23
                $str2,
9038 23
                true,
9039 23
                false,
9040 23
                $encoding,
9041 23
                null,
9042 23
                false
9043
            )
9044
        );
9045
    }
9046
9047
    /**
9048
     * alias for "UTF8::strstr()"
9049
     *
9050
     * @param string $haystack
9051
     * @param string $needle
9052
     * @param bool   $before_needle
9053
     * @param string $encoding
9054
     * @param bool   $clean_utf8
9055
     *
9056
     * @psalm-pure
9057
     *
9058
     * @return false|string
9059
     *
9060
     * @see        UTF8::strstr()
9061
     * @deprecated <p>please use "UTF8::strstr()"</p>
9062
     */
9063 2
    public static function strchr(
9064
        string $haystack,
9065
        string $needle,
9066
        bool $before_needle = false,
9067
        string $encoding = 'UTF-8',
9068
        bool $clean_utf8 = false
9069
    ) {
9070 2
        return self::strstr(
9071 2
            $haystack,
9072 2
            $needle,
9073 2
            $before_needle,
9074 2
            $encoding,
9075 2
            $clean_utf8
9076
        );
9077
    }
9078
9079
    /**
9080
     * Case-sensitive string comparison.
9081
     *
9082
     * @param string $str1 <p>The first string.</p>
9083
     * @param string $str2 <p>The second string.</p>
9084
     *
9085
     * @psalm-pure
9086
     *
9087
     * @return int
9088
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
9089
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
9090
     *             <strong>0</strong> if they are equal
9091
     */
9092 29
    public static function strcmp(string $str1, string $str2): int
9093
    {
9094 29
        if ($str1 === $str2) {
9095 21
            return 0;
9096
        }
9097
9098 24
        return \strcmp(
9099 24
            \Normalizer::normalize($str1, \Normalizer::NFD),
9100 24
            \Normalizer::normalize($str2, \Normalizer::NFD)
9101
        );
9102
    }
9103
9104
    /**
9105
     * Find length of initial segment not matching mask.
9106
     *
9107
     * @param string $str
9108
     * @param string $char_list
9109
     * @param int    $offset
9110
     * @param int    $length
9111
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
9112
     *
9113
     * @psalm-pure
9114
     *
9115
     * @return int
9116
     */
9117 12
    public static function strcspn(
9118
        string $str,
9119
        string $char_list,
9120
        int $offset = null,
9121
        int $length = null,
9122
        string $encoding = 'UTF-8'
9123
    ): int {
9124 12
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9125
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9126
        }
9127
9128 12
        if ($char_list === '') {
9129 2
            return (int) self::strlen($str, $encoding);
9130
        }
9131
9132 11
        if ($offset !== null || $length !== null) {
9133 3
            if ($encoding === 'UTF-8') {
9134 3
                if ($length === null) {
9135
                    /** @noinspection UnnecessaryCastingInspection */
9136 2
                    $str_tmp = \mb_substr($str, (int) $offset);
9137
                } else {
9138
                    /** @noinspection UnnecessaryCastingInspection */
9139 3
                    $str_tmp = \mb_substr($str, (int) $offset, $length);
9140
                }
9141
            } else {
9142
                /** @noinspection UnnecessaryCastingInspection */
9143
                $str_tmp = self::substr($str, (int) $offset, $length, $encoding);
9144
            }
9145
9146 3
            if ($str_tmp === false) {
9147
                return 0;
9148
            }
9149
9150
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
9151 3
            $str = $str_tmp;
9152
        }
9153
9154 11
        if ($str === '') {
9155 2
            return 0;
9156
        }
9157
9158 10
        $matches = [];
9159 10
        if (\preg_match('/^(.*?)' . self::rxClass($char_list) . '/us', $str, $matches)) {
9160 9
            $return = self::strlen($matches[1], $encoding);
9161 9
            if ($return === false) {
9162
                return 0;
9163
            }
9164
9165 9
            return $return;
9166
        }
9167
9168 2
        return (int) self::strlen($str, $encoding);
9169
    }
9170
9171
    /**
9172
     * alias for "UTF8::stristr()"
9173
     *
9174
     * @param string $haystack
9175
     * @param string $needle
9176
     * @param bool   $before_needle
9177
     * @param string $encoding
9178
     * @param bool   $clean_utf8
9179
     *
9180
     * @psalm-pure
9181
     *
9182
     * @return false|string
9183
     *
9184
     * @see        UTF8::stristr()
9185
     * @deprecated <p>please use "UTF8::stristr()"</p>
9186
     */
9187 1
    public static function strichr(
9188
        string $haystack,
9189
        string $needle,
9190
        bool $before_needle = false,
9191
        string $encoding = 'UTF-8',
9192
        bool $clean_utf8 = false
9193
    ) {
9194 1
        return self::stristr(
9195 1
            $haystack,
9196 1
            $needle,
9197 1
            $before_needle,
9198 1
            $encoding,
9199 1
            $clean_utf8
9200
        );
9201
    }
9202
9203
    /**
9204
     * Create a UTF-8 string from code points.
9205
     *
9206
     * INFO: opposite to UTF8::codepoints()
9207
     *
9208
     * @param array $array <p>Integer or Hexadecimal codepoints.</p>
9209
     *
9210
     * @psalm-pure
9211
     *
9212
     * @return string
9213
     *                <p>A UTF-8 encoded string.</p>
9214
     */
9215 4
    public static function string(array $array): string
9216
    {
9217 4
        if ($array === []) {
9218 4
            return '';
9219
        }
9220
9221 4
        $str = '';
9222 4
        foreach ($array as $strPart) {
9223 4
            $str .= '&#' . (int) $strPart . ';';
9224
        }
9225
9226 4
        return self::html_entity_decode($str, \ENT_QUOTES | \ENT_HTML5);
9227
    }
9228
9229
    /**
9230
     * Checks if string starts with "BOM" (Byte Order Mark Character) character.
9231
     *
9232
     * @param string $str <p>The input string.</p>
9233
     *
9234
     * @psalm-pure
9235
     *
9236
     * @return bool
9237
     *              <strong>true</strong> if the string has BOM at the start,<br>
9238
     *              <strong>false</strong> otherwise
9239
     */
9240 6
    public static function string_has_bom(string $str): bool
9241
    {
9242
        /** @noinspection PhpUnusedLocalVariableInspection */
9243 6
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
9244 6
            if (\strpos($str, $bom_string) === 0) {
9245 6
                return true;
9246
            }
9247
        }
9248
9249 6
        return false;
9250
    }
9251
9252
    /**
9253
     * Strip HTML and PHP tags from a string + clean invalid UTF-8.
9254
     *
9255
     * @see http://php.net/manual/en/function.strip-tags.php
9256
     *
9257
     * @param string $str            <p>
9258
     *                               The input string.
9259
     *                               </p>
9260
     * @param string $allowable_tags [optional] <p>
9261
     *                               You can use the optional second parameter to specify tags which should
9262
     *                               not be stripped.
9263
     *                               </p>
9264
     *                               <p>
9265
     *                               HTML comments and PHP tags are also stripped. This is hardcoded and
9266
     *                               can not be changed with allowable_tags.
9267
     *                               </p>
9268
     * @param bool   $clean_utf8     [optional] <p>Remove non UTF-8 chars from the string.</p>
9269
     *
9270
     * @psalm-pure
9271
     *
9272
     * @return string
9273
     *                <p>The stripped string.</p>
9274
     */
9275 4
    public static function strip_tags(
9276
        string $str,
9277
        string $allowable_tags = null,
9278
        bool $clean_utf8 = false
9279
    ): string {
9280 4
        if ($str === '') {
9281 1
            return '';
9282
        }
9283
9284 4
        if ($clean_utf8 === true) {
9285 2
            $str = self::clean($str);
9286
        }
9287
9288 4
        if ($allowable_tags === null) {
9289 4
            return \strip_tags($str);
9290
        }
9291
9292 2
        return \strip_tags($str, $allowable_tags);
9293
    }
9294
9295
    /**
9296
     * Strip all whitespace characters. This includes tabs and newline
9297
     * characters, as well as multibyte whitespace such as the thin space
9298
     * and ideographic space.
9299
     *
9300
     * @param string $str
9301
     *
9302
     * @psalm-pure
9303
     *
9304
     * @return string
9305
     */
9306 36
    public static function strip_whitespace(string $str): string
9307
    {
9308 36
        if ($str === '') {
9309 3
            return '';
9310
        }
9311
9312 33
        return (string) \preg_replace('/[[:space:]]+/u', '', $str);
9313
    }
9314
9315
    /**
9316
     * Find the position of the first occurrence of a substring in a string, case-insensitive.
9317
     *
9318
     * @see http://php.net/manual/en/function.mb-stripos.php
9319
     *
9320
     * @param string $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
9321
     * @param string $needle     <p>The string to find in haystack.</p>
9322
     * @param int    $offset     [optional] <p>The position in haystack to start searching.</p>
9323
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9324
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9325
     *
9326
     * @psalm-pure
9327
     *
9328
     * @return false|int
9329
     *                   Return the <strong>(int)</strong> numeric position of the first occurrence of needle in the
9330
     *                   haystack string,<br> or <strong>false</strong> if needle is not found
9331
     */
9332 24
    public static function stripos(
9333
        string $haystack,
9334
        string $needle,
9335
        int $offset = 0,
9336
        $encoding = 'UTF-8',
9337
        bool $clean_utf8 = false
9338
    ) {
9339 24
        if ($haystack === '' || $needle === '') {
9340 5
            return false;
9341
        }
9342
9343 23
        if ($clean_utf8 === true) {
9344
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9345
            // if invalid characters are found in $haystack before $needle
9346 1
            $haystack = self::clean($haystack);
9347 1
            $needle = self::clean($needle);
9348
        }
9349
9350 23
        if (self::$SUPPORT['mbstring'] === true) {
9351 23
            if ($encoding === 'UTF-8') {
9352 23
                return \mb_stripos($haystack, $needle, $offset);
9353
            }
9354
9355 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9356
9357 3
            return \mb_stripos($haystack, $needle, $offset, $encoding);
9358
        }
9359
9360 2
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
9361
9362
        if (
9363 2
            $encoding === 'UTF-8' // INFO: "grapheme_stripos()" can't handle other encodings
9364
            &&
9365 2
            $offset >= 0 // grapheme_stripos() can't handle negative offset
9366
            &&
9367 2
            self::$SUPPORT['intl'] === true
9368
        ) {
9369
            $return_tmp = \grapheme_stripos($haystack, $needle, $offset);
9370
            if ($return_tmp !== false) {
9371
                return $return_tmp;
9372
            }
9373
        }
9374
9375
        //
9376
        // fallback for ascii only
9377
        //
9378
9379 2
        if (ASCII::is_ascii($haystack . $needle)) {
9380
            return \stripos($haystack, $needle, $offset);
9381
        }
9382
9383
        //
9384
        // fallback via vanilla php
9385
        //
9386
9387 2
        $haystack = self::strtocasefold($haystack, true, false, $encoding, null, false);
9388 2
        $needle = self::strtocasefold($needle, true, false, $encoding, null, false);
9389
9390 2
        return self::strpos($haystack, $needle, $offset, $encoding);
9391
    }
9392
9393
    /**
9394
     * Returns all of haystack starting from and including the first occurrence of needle to the end.
9395
     *
9396
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
9397
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
9398
     * @param bool   $before_needle [optional] <p>
9399
     *                              If <b>TRUE</b>, it returns the part of the
9400
     *                              haystack before the first occurrence of the needle (excluding the needle).
9401
     *                              </p>
9402
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9403
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9404
     *
9405
     * @psalm-pure
9406
     *
9407
     * @return false|string
9408
     *                      <p>A sub-string,<br>or <strong>false</strong> if needle is not found.</p>
9409
     */
9410 12
    public static function stristr(
9411
        string $haystack,
9412
        string $needle,
9413
        bool $before_needle = false,
9414
        string $encoding = 'UTF-8',
9415
        bool $clean_utf8 = false
9416
    ) {
9417 12
        if ($haystack === '' || $needle === '') {
9418 3
            return false;
9419
        }
9420
9421 9
        if ($clean_utf8 === true) {
9422
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9423
            // if invalid characters are found in $haystack before $needle
9424 1
            $needle = self::clean($needle);
9425 1
            $haystack = self::clean($haystack);
9426
        }
9427
9428 9
        if (!$needle) {
9429
            return $haystack;
9430
        }
9431
9432 9
        if (self::$SUPPORT['mbstring'] === true) {
9433 9
            if ($encoding === 'UTF-8') {
9434 9
                return \mb_stristr($haystack, $needle, $before_needle);
9435
            }
9436
9437 1
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9438
9439 1
            return \mb_stristr($haystack, $needle, $before_needle, $encoding);
9440
        }
9441
9442
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
9443
9444
        if (
9445
            $encoding !== 'UTF-8'
9446
            &&
9447
            self::$SUPPORT['mbstring'] === false
9448
        ) {
9449
            /**
9450
             * @psalm-suppress ImpureFunctionCall - is is only a warning
9451
             */
9452
            \trigger_error('UTF8::stristr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9453
        }
9454
9455
        if (
9456
            $encoding === 'UTF-8' // INFO: "grapheme_stristr()" can't handle other encodings
9457
            &&
9458
            self::$SUPPORT['intl'] === true
9459
        ) {
9460
            $return_tmp = \grapheme_stristr($haystack, $needle, $before_needle);
9461
            if ($return_tmp !== false) {
9462
                return $return_tmp;
9463
            }
9464
        }
9465
9466
        if (ASCII::is_ascii($needle . $haystack)) {
9467
            return \stristr($haystack, $needle, $before_needle);
9468
        }
9469
9470
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/usi', $haystack, $match);
9471
9472
        if (!isset($match[1])) {
9473
            return false;
9474
        }
9475
9476
        if ($before_needle) {
9477
            return $match[1];
9478
        }
9479
9480
        return self::substr($haystack, (int) self::strlen($match[1], $encoding), null, $encoding);
9481
    }
9482
9483
    /**
9484
     * Get the string length, not the byte-length!
9485
     *
9486
     * @see http://php.net/manual/en/function.mb-strlen.php
9487
     *
9488
     * @param string $str        <p>The string being checked for length.</p>
9489
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9490
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9491
     *
9492
     * @psalm-pure
9493
     *
9494
     * @return false|int
9495
     *                   <p>
9496
     *                   The number <strong>(int)</strong> of characters in the string $str having character encoding
9497
     *                   $encoding.
9498
     *                   (One multi-byte character counted as +1).
9499
     *                   <br>
9500
     *                   Can return <strong>false</strong>, if e.g. mbstring is not installed and we process invalid
9501
     *                   chars.
9502
     *                   </p>
9503
     */
9504 173
    public static function strlen(
9505
        string $str,
9506
        string $encoding = 'UTF-8',
9507
        bool $clean_utf8 = false
9508
    ) {
9509 173
        if ($str === '') {
9510 21
            return 0;
9511
        }
9512
9513 171
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9514 12
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9515
        }
9516
9517 171
        if ($clean_utf8 === true) {
9518
            // "mb_strlen" and "\iconv_strlen" returns wrong length,
9519
            // if invalid characters are found in $str
9520 4
            $str = self::clean($str);
9521
        }
9522
9523
        //
9524
        // fallback via mbstring
9525
        //
9526
9527 171
        if (self::$SUPPORT['mbstring'] === true) {
9528 165
            if ($encoding === 'UTF-8') {
9529 165
                return \mb_strlen($str);
9530
            }
9531
9532 4
            return \mb_strlen($str, $encoding);
9533
        }
9534
9535
        //
9536
        // fallback for binary || ascii only
9537
        //
9538
9539
        if (
9540 8
            $encoding === 'CP850'
9541
            ||
9542 8
            $encoding === 'ASCII'
9543
        ) {
9544
            return \strlen($str);
9545
        }
9546
9547
        if (
9548 8
            $encoding !== 'UTF-8'
9549
            &&
9550 8
            self::$SUPPORT['mbstring'] === false
9551
            &&
9552 8
            self::$SUPPORT['iconv'] === false
9553
        ) {
9554
            /**
9555
             * @psalm-suppress ImpureFunctionCall - is is only a warning
9556
             */
9557 2
            \trigger_error('UTF8::strlen() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9558
        }
9559
9560
        //
9561
        // fallback via iconv
9562
        //
9563
9564 8
        if (self::$SUPPORT['iconv'] === true) {
9565
            $return_tmp = \iconv_strlen($str, $encoding);
9566
            if ($return_tmp !== false) {
9567
                return $return_tmp;
9568
            }
9569
        }
9570
9571
        //
9572
        // fallback via intl
9573
        //
9574
9575
        if (
9576 8
            $encoding === 'UTF-8' // INFO: "grapheme_strlen()" can't handle other encodings
9577
            &&
9578 8
            self::$SUPPORT['intl'] === true
9579
        ) {
9580
            $return_tmp = \grapheme_strlen($str);
9581
            if ($return_tmp !== null) {
9582
                return $return_tmp;
9583
            }
9584
        }
9585
9586
        //
9587
        // fallback for ascii only
9588
        //
9589
9590 8
        if (ASCII::is_ascii($str)) {
9591 4
            return \strlen($str);
9592
        }
9593
9594
        //
9595
        // fallback via vanilla php
9596
        //
9597
9598 8
        \preg_match_all('/./us', $str, $parts);
9599
9600 8
        $return_tmp = \count($parts[0]);
9601 8
        if ($return_tmp === 0) {
9602
            return false;
9603
        }
9604
9605 8
        return $return_tmp;
9606
    }
9607
9608
    /**
9609
     * Get string length in byte.
9610
     *
9611
     * @param string $str
9612
     *
9613
     * @psalm-pure
9614
     *
9615
     * @return int
9616
     */
9617
    public static function strlen_in_byte(string $str): int
9618
    {
9619
        if ($str === '') {
9620
            return 0;
9621
        }
9622
9623
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9624
            // "mb_" is available if overload is used, so use it ...
9625
            return \mb_strlen($str, 'CP850'); // 8-BIT
9626
        }
9627
9628
        return \strlen($str);
9629
    }
9630
9631
    /**
9632
     * Case-insensitive string comparisons using a "natural order" algorithm.
9633
     *
9634
     * INFO: natural order version of UTF8::strcasecmp()
9635
     *
9636
     * @param string $str1     <p>The first string.</p>
9637
     * @param string $str2     <p>The second string.</p>
9638
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9639
     *
9640
     * @psalm-pure
9641
     *
9642
     * @return int
9643
     *             <strong>&lt; 0</strong> if str1 is less than str2<br>
9644
     *             <strong>&gt; 0</strong> if str1 is greater than str2<br>
9645
     *             <strong>0</strong> if they are equal
9646
     */
9647 2
    public static function strnatcasecmp(string $str1, string $str2, string $encoding = 'UTF-8'): int
9648
    {
9649 2
        return self::strnatcmp(
9650 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
9651 2
            self::strtocasefold($str2, true, false, $encoding, null, false)
9652
        );
9653
    }
9654
9655
    /**
9656
     * String comparisons using a "natural order" algorithm
9657
     *
9658
     * INFO: natural order version of UTF8::strcmp()
9659
     *
9660
     * @see http://php.net/manual/en/function.strnatcmp.php
9661
     *
9662
     * @param string $str1 <p>The first string.</p>
9663
     * @param string $str2 <p>The second string.</p>
9664
     *
9665
     * @psalm-pure
9666
     *
9667
     * @return int
9668
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
9669
     *             <strong>&gt; 0</strong> if str1 is greater than str2;<br>
9670
     *             <strong>0</strong> if they are equal
9671
     */
9672 4
    public static function strnatcmp(string $str1, string $str2): int
9673
    {
9674 4
        if ($str1 === $str2) {
9675 4
            return 0;
9676
        }
9677
9678 4
        return \strnatcmp(
9679 4
            (string) self::strtonatfold($str1),
9680 4
            (string) self::strtonatfold($str2)
9681
        );
9682
    }
9683
9684
    /**
9685
     * Case-insensitive string comparison of the first n characters.
9686
     *
9687
     * @see http://php.net/manual/en/function.strncasecmp.php
9688
     *
9689
     * @param string $str1     <p>The first string.</p>
9690
     * @param string $str2     <p>The second string.</p>
9691
     * @param int    $len      <p>The length of strings to be used in the comparison.</p>
9692
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9693
     *
9694
     * @psalm-pure
9695
     *
9696
     * @return int
9697
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
9698
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
9699
     *             <strong>0</strong> if they are equal
9700
     */
9701 2
    public static function strncasecmp(
9702
        string $str1,
9703
        string $str2,
9704
        int $len,
9705
        string $encoding = 'UTF-8'
9706
    ): int {
9707 2
        return self::strncmp(
9708 2
            self::strtocasefold($str1, true, false, $encoding, null, false),
9709 2
            self::strtocasefold($str2, true, false, $encoding, null, false),
9710 2
            $len
9711
        );
9712
    }
9713
9714
    /**
9715
     * String comparison of the first n characters.
9716
     *
9717
     * @see http://php.net/manual/en/function.strncmp.php
9718
     *
9719
     * @param string $str1     <p>The first string.</p>
9720
     * @param string $str2     <p>The second string.</p>
9721
     * @param int    $len      <p>Number of characters to use in the comparison.</p>
9722
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
9723
     *
9724
     * @psalm-pure
9725
     *
9726
     * @return int
9727
     *             <strong>&lt; 0</strong> if <i>str1</i> is less than <i>str2</i>;<br>
9728
     *             <strong>&gt; 0</strong> if <i>str1</i> is greater than <i>str2</i>;<br>
9729
     *             <strong>0</strong> if they are equal
9730
     */
9731 4
    public static function strncmp(
9732
        string $str1,
9733
        string $str2,
9734
        int $len,
9735
        string $encoding = 'UTF-8'
9736
    ): int {
9737 4
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9738
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9739
        }
9740
9741 4
        if ($encoding === 'UTF-8') {
9742 4
            $str1 = (string) \mb_substr($str1, 0, $len);
9743 4
            $str2 = (string) \mb_substr($str2, 0, $len);
9744
        } else {
9745
            $str1 = (string) self::substr($str1, 0, $len, $encoding);
9746
            $str2 = (string) self::substr($str2, 0, $len, $encoding);
9747
        }
9748
9749 4
        return self::strcmp($str1, $str2);
9750
    }
9751
9752
    /**
9753
     * Search a string for any of a set of characters.
9754
     *
9755
     * @see http://php.net/manual/en/function.strpbrk.php
9756
     *
9757
     * @param string $haystack  <p>The string where char_list is looked for.</p>
9758
     * @param string $char_list <p>This parameter is case-sensitive.</p>
9759
     *
9760
     * @psalm-pure
9761
     *
9762
     * @return false|string string starting from the character found, or false if it is not found
9763
     */
9764 2
    public static function strpbrk(string $haystack, string $char_list)
9765
    {
9766 2
        if ($haystack === '' || $char_list === '') {
9767 2
            return false;
9768
        }
9769
9770 2
        if (\preg_match('/' . self::rxClass($char_list) . '/us', $haystack, $m)) {
9771 2
            return \substr($haystack, (int) \strpos($haystack, $m[0]));
9772
        }
9773
9774 2
        return false;
9775
    }
9776
9777
    /**
9778
     * Find the position of the first occurrence of a substring in a string.
9779
     *
9780
     * @see http://php.net/manual/en/function.mb-strpos.php
9781
     *
9782
     * @param string     $haystack   <p>The string from which to get the position of the first occurrence of needle.</p>
9783
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
9784
     * @param int        $offset     [optional] <p>The search offset. If it is not specified, 0 is used.</p>
9785
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
9786
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
9787
     *
9788
     * @psalm-pure
9789
     *
9790
     * @return false|int
9791
     *                   The <strong>(int)</strong> numeric position of the first occurrence of needle in the haystack
9792
     *                   string.<br> If needle is not found it returns false.
9793
     */
9794 53
    public static function strpos(
9795
        string $haystack,
9796
        $needle,
9797
        int $offset = 0,
9798
        $encoding = 'UTF-8',
9799
        bool $clean_utf8 = false
9800
    ) {
9801 53
        if ($haystack === '') {
9802 4
            return false;
9803
        }
9804
9805
        // iconv and mbstring do not support integer $needle
9806 52
        if ((int) $needle === $needle) {
9807
            $needle = (string) self::chr($needle);
9808
        }
9809 52
        $needle = (string) $needle;
9810
9811 52
        if ($needle === '') {
9812 2
            return false;
9813
        }
9814
9815 52
        if ($clean_utf8 === true) {
9816
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
9817
            // if invalid characters are found in $haystack before $needle
9818 3
            $needle = self::clean($needle);
9819 3
            $haystack = self::clean($haystack);
9820
        }
9821
9822 52
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9823 11
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9824
        }
9825
9826
        //
9827
        // fallback via mbstring
9828
        //
9829
9830 52
        if (self::$SUPPORT['mbstring'] === true) {
9831 50
            if ($encoding === 'UTF-8') {
9832 50
                return \mb_strpos($haystack, $needle, $offset);
9833
            }
9834
9835 2
            return \mb_strpos($haystack, $needle, $offset, $encoding);
9836
        }
9837
9838
        //
9839
        // fallback for binary || ascii only
9840
        //
9841
        if (
9842 4
            $encoding === 'CP850'
9843
            ||
9844 4
            $encoding === 'ASCII'
9845
        ) {
9846 2
            return \strpos($haystack, $needle, $offset);
9847
        }
9848
9849
        if (
9850 4
            $encoding !== 'UTF-8'
9851
            &&
9852 4
            self::$SUPPORT['iconv'] === false
9853
            &&
9854 4
            self::$SUPPORT['mbstring'] === false
9855
        ) {
9856
            /**
9857
             * @psalm-suppress ImpureFunctionCall - is is only a warning
9858
             */
9859 2
            \trigger_error('UTF8::strpos() without mbstring / iconv cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
9860
        }
9861
9862
        //
9863
        // fallback via intl
9864
        //
9865
9866
        if (
9867 4
            $encoding === 'UTF-8' // INFO: "grapheme_strpos()" can't handle other encodings
9868
            &&
9869 4
            $offset >= 0 // grapheme_strpos() can't handle negative offset
9870
            &&
9871 4
            self::$SUPPORT['intl'] === true
9872
        ) {
9873
            $return_tmp = \grapheme_strpos($haystack, $needle, $offset);
9874
            if ($return_tmp !== false) {
9875
                return $return_tmp;
9876
            }
9877
        }
9878
9879
        //
9880
        // fallback via iconv
9881
        //
9882
9883
        if (
9884 4
            $offset >= 0 // iconv_strpos() can't handle negative offset
9885
            &&
9886 4
            self::$SUPPORT['iconv'] === true
9887
        ) {
9888
            // ignore invalid negative offset to keep compatibility
9889
            // with php < 5.5.35, < 5.6.21, < 7.0.6
9890
            $return_tmp = \iconv_strpos($haystack, $needle, $offset > 0 ? $offset : 0, $encoding);
9891
            if ($return_tmp !== false) {
9892
                return $return_tmp;
9893
            }
9894
        }
9895
9896
        //
9897
        // fallback for ascii only
9898
        //
9899
9900 4
        if (ASCII::is_ascii($haystack . $needle)) {
9901 2
            return \strpos($haystack, $needle, $offset);
9902
        }
9903
9904
        //
9905
        // fallback via vanilla php
9906
        //
9907
9908 4
        $haystack_tmp = self::substr($haystack, $offset, null, $encoding);
9909 4
        if ($haystack_tmp === false) {
9910
            $haystack_tmp = '';
9911
        }
9912 4
        $haystack = (string) $haystack_tmp;
9913
9914 4
        if ($offset < 0) {
9915
            $offset = 0;
9916
        }
9917
9918 4
        $pos = \strpos($haystack, $needle);
9919 4
        if ($pos === false) {
9920 2
            return false;
9921
        }
9922
9923 4
        if ($pos) {
9924 4
            return $offset + (int) self::strlen(\substr($haystack, 0, $pos), $encoding);
9925
        }
9926
9927 2
        return $offset + 0;
9928
    }
9929
9930
    /**
9931
     * Find the position of the first occurrence of a substring in a string.
9932
     *
9933
     * @param string $haystack <p>
9934
     *                         The string being checked.
9935
     *                         </p>
9936
     * @param string $needle   <p>
9937
     *                         The position counted from the beginning of haystack.
9938
     *                         </p>
9939
     * @param int    $offset   [optional] <p>
9940
     *                         The search offset. If it is not specified, 0 is used.
9941
     *                         </p>
9942
     *
9943
     * @psalm-pure
9944
     *
9945
     * @return false|int The numeric position of the first occurrence of needle in the
9946
     *                   haystack string. If needle is not found, it returns false.
9947
     */
9948
    public static function strpos_in_byte(string $haystack, string $needle, int $offset = 0)
9949
    {
9950
        if ($haystack === '' || $needle === '') {
9951
            return false;
9952
        }
9953
9954
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
9955
            // "mb_" is available if overload is used, so use it ...
9956
            return \mb_strpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
9957
        }
9958
9959
        return \strpos($haystack, $needle, $offset);
9960
    }
9961
9962
    /**
9963
     * Find the last occurrence of a character in a string within another.
9964
     *
9965
     * @see http://php.net/manual/en/function.mb-strrchr.php
9966
     *
9967
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
9968
     * @param string $needle        <p>The string to find in haystack</p>
9969
     * @param bool   $before_needle [optional] <p>
9970
     *                              Determines which portion of haystack
9971
     *                              this function returns.
9972
     *                              If set to true, it returns all of haystack
9973
     *                              from the beginning to the last occurrence of needle.
9974
     *                              If set to false, it returns all of haystack
9975
     *                              from the last occurrence of needle to the end,
9976
     *                              </p>
9977
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
9978
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
9979
     *
9980
     * @psalm-pure
9981
     *
9982
     * @return false|string the portion of haystack or false if needle is not found
9983
     */
9984 2
    public static function strrchr(
9985
        string $haystack,
9986
        string $needle,
9987
        bool $before_needle = false,
9988
        string $encoding = 'UTF-8',
9989
        bool $clean_utf8 = false
9990
    ) {
9991 2
        if ($haystack === '' || $needle === '') {
9992 2
            return false;
9993
        }
9994
9995 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
9996 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
9997
        }
9998
9999 2
        if ($clean_utf8 === true) {
10000
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10001
            // if invalid characters are found in $haystack before $needle
10002 2
            $needle = self::clean($needle);
10003 2
            $haystack = self::clean($haystack);
10004
        }
10005
10006
        //
10007
        // fallback via mbstring
10008
        //
10009
10010 2
        if (self::$SUPPORT['mbstring'] === true) {
10011 2
            if ($encoding === 'UTF-8') {
10012 2
                return \mb_strrchr($haystack, $needle, $before_needle);
10013
            }
10014
10015 2
            return \mb_strrchr($haystack, $needle, $before_needle, $encoding);
10016
        }
10017
10018
        //
10019
        // fallback for binary || ascii only
10020
        //
10021
10022
        if (
10023
            $before_needle === false
10024
            &&
10025
            (
10026
                $encoding === 'CP850'
10027
                ||
10028
                $encoding === 'ASCII'
10029
            )
10030
        ) {
10031
            return \strrchr($haystack, $needle);
10032
        }
10033
10034
        if (
10035
            $encoding !== 'UTF-8'
10036
            &&
10037
            self::$SUPPORT['mbstring'] === false
10038
        ) {
10039
            /**
10040
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10041
             */
10042
            \trigger_error('UTF8::strrchr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10043
        }
10044
10045
        //
10046
        // fallback via iconv
10047
        //
10048
10049
        if (self::$SUPPORT['iconv'] === true) {
10050
            $needle_tmp = self::substr($needle, 0, 1, $encoding);
10051
            if ($needle_tmp === false) {
10052
                return false;
10053
            }
10054
            $needle = (string) $needle_tmp;
10055
10056
            $pos = \iconv_strrpos($haystack, $needle, $encoding);
10057
            if ($pos === false) {
10058
                return false;
10059
            }
10060
10061
            if ($before_needle) {
10062
                return self::substr($haystack, 0, $pos, $encoding);
10063
            }
10064
10065
            return self::substr($haystack, $pos, null, $encoding);
10066
        }
10067
10068
        //
10069
        // fallback via vanilla php
10070
        //
10071
10072
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
10073
        if ($needle_tmp === false) {
10074
            return false;
10075
        }
10076
        $needle = (string) $needle_tmp;
10077
10078
        $pos = self::strrpos($haystack, $needle, 0, $encoding);
10079
        if ($pos === false) {
10080
            return false;
10081
        }
10082
10083
        if ($before_needle) {
10084
            return self::substr($haystack, 0, $pos, $encoding);
10085
        }
10086
10087
        return self::substr($haystack, $pos, null, $encoding);
10088
    }
10089
10090
    /**
10091
     * Reverses characters order in the string.
10092
     *
10093
     * @param string $str      <p>The input string.</p>
10094
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
10095
     *
10096
     * @psalm-pure
10097
     *
10098
     * @return string the string with characters in the reverse sequence
10099
     */
10100 10
    public static function strrev(string $str, string $encoding = 'UTF-8'): string
10101
    {
10102 10
        if ($str === '') {
10103 4
            return '';
10104
        }
10105
10106
        // init
10107 8
        $reversed = '';
10108
10109 8
        $str = self::emoji_encode($str, true);
10110
10111 8
        if ($encoding === 'UTF-8') {
10112 8
            if (self::$SUPPORT['intl'] === true) {
10113
                // try "grapheme" first: https://stackoverflow.com/questions/17496493/strrev-dosent-support-utf-8
10114 8
                $i = (int) \grapheme_strlen($str);
10115 8
                while ($i--) {
10116 8
                    $reversed_tmp = \grapheme_substr($str, $i, 1);
10117 8
                    if ($reversed_tmp !== false) {
10118 8
                        $reversed .= $reversed_tmp;
10119
                    }
10120
                }
10121
            } else {
10122
                $i = (int) \mb_strlen($str);
10123 8
                while ($i--) {
10124
                    $reversed_tmp = \mb_substr($str, $i, 1);
10125
                    if ($reversed_tmp !== false) {
10126
                        $reversed .= $reversed_tmp;
10127
                    }
10128
                }
10129
            }
10130
        } else {
10131
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10132
10133
            $i = (int) self::strlen($str, $encoding);
10134
            while ($i--) {
10135
                $reversed_tmp = self::substr($str, $i, 1, $encoding);
10136
                if ($reversed_tmp !== false) {
10137
                    $reversed .= $reversed_tmp;
10138
                }
10139
            }
10140
        }
10141
10142 8
        return self::emoji_decode($reversed, true);
10143
    }
10144
10145
    /**
10146
     * Find the last occurrence of a character in a string within another, case-insensitive.
10147
     *
10148
     * @see http://php.net/manual/en/function.mb-strrichr.php
10149
     *
10150
     * @param string $haystack      <p>The string from which to get the last occurrence of needle.</p>
10151
     * @param string $needle        <p>The string to find in haystack.</p>
10152
     * @param bool   $before_needle [optional] <p>
10153
     *                              Determines which portion of haystack
10154
     *                              this function returns.
10155
     *                              If set to true, it returns all of haystack
10156
     *                              from the beginning to the last occurrence of needle.
10157
     *                              If set to false, it returns all of haystack
10158
     *                              from the last occurrence of needle to the end,
10159
     *                              </p>
10160
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
10161
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
10162
     *
10163
     * @psalm-pure
10164
     *
10165
     * @return false|string the portion of haystack or<br>false if needle is not found
10166
     */
10167 3
    public static function strrichr(
10168
        string $haystack,
10169
        string $needle,
10170
        bool $before_needle = false,
10171
        string $encoding = 'UTF-8',
10172
        bool $clean_utf8 = false
10173
    ) {
10174 3
        if ($haystack === '' || $needle === '') {
10175 2
            return false;
10176
        }
10177
10178 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10179 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10180
        }
10181
10182 3
        if ($clean_utf8 === true) {
10183
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10184
            // if invalid characters are found in $haystack before $needle
10185 2
            $needle = self::clean($needle);
10186 2
            $haystack = self::clean($haystack);
10187
        }
10188
10189
        //
10190
        // fallback via mbstring
10191
        //
10192
10193 3
        if (self::$SUPPORT['mbstring'] === true) {
10194 3
            if ($encoding === 'UTF-8') {
10195 3
                return \mb_strrichr($haystack, $needle, $before_needle);
10196
            }
10197
10198 2
            return \mb_strrichr($haystack, $needle, $before_needle, $encoding);
10199
        }
10200
10201
        //
10202
        // fallback via vanilla php
10203
        //
10204
10205
        $needle_tmp = self::substr($needle, 0, 1, $encoding);
10206
        if ($needle_tmp === false) {
10207
            return false;
10208
        }
10209
        $needle = (string) $needle_tmp;
10210
10211
        $pos = self::strripos($haystack, $needle, 0, $encoding);
10212
        if ($pos === false) {
10213
            return false;
10214
        }
10215
10216
        if ($before_needle) {
10217
            return self::substr($haystack, 0, $pos, $encoding);
10218
        }
10219
10220
        return self::substr($haystack, $pos, null, $encoding);
10221
    }
10222
10223
    /**
10224
     * Find the position of the last occurrence of a substring in a string, case-insensitive.
10225
     *
10226
     * @param string     $haystack   <p>The string to look in.</p>
10227
     * @param int|string $needle     <p>The string to look for.</p>
10228
     * @param int        $offset     [optional] <p>Number of characters to ignore in the beginning or end.</p>
10229
     * @param string     $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
10230
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10231
     *
10232
     * @psalm-pure
10233
     *
10234
     * @return false|int
10235
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
10236
     *                   string.<br>If needle is not found, it returns false.</p>
10237
     */
10238 3
    public static function strripos(
10239
        string $haystack,
10240
        $needle,
10241
        int $offset = 0,
10242
        string $encoding = 'UTF-8',
10243
        bool $clean_utf8 = false
10244
    ) {
10245 3
        if ($haystack === '') {
10246
            return false;
10247
        }
10248
10249
        // iconv and mbstring do not support integer $needle
10250 3
        if ((int) $needle === $needle && $needle >= 0) {
10251
            $needle = (string) self::chr($needle);
10252
        }
10253 3
        $needle = (string) $needle;
10254
10255 3
        if ($needle === '') {
10256
            return false;
10257
        }
10258
10259 3
        if ($clean_utf8 === true) {
10260
            // mb_strripos() && iconv_strripos() is not tolerant to invalid characters
10261 2
            $needle = self::clean($needle);
10262 2
            $haystack = self::clean($haystack);
10263
        }
10264
10265 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10266 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10267
        }
10268
10269
        //
10270
        // fallback via mbstrig
10271
        //
10272
10273 3
        if (self::$SUPPORT['mbstring'] === true) {
10274 3
            if ($encoding === 'UTF-8') {
10275 3
                return \mb_strripos($haystack, $needle, $offset);
10276
            }
10277
10278
            return \mb_strripos($haystack, $needle, $offset, $encoding);
10279
        }
10280
10281
        //
10282
        // fallback for binary || ascii only
10283
        //
10284
10285
        if (
10286
            $encoding === 'CP850'
10287
            ||
10288
            $encoding === 'ASCII'
10289
        ) {
10290
            return \strripos($haystack, $needle, $offset);
10291
        }
10292
10293
        if (
10294
            $encoding !== 'UTF-8'
10295
            &&
10296
            self::$SUPPORT['mbstring'] === false
10297
        ) {
10298
            /**
10299
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10300
             */
10301
            \trigger_error('UTF8::strripos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10302
        }
10303
10304
        //
10305
        // fallback via intl
10306
        //
10307
10308
        if (
10309
            $encoding === 'UTF-8' // INFO: "grapheme_strripos()" can't handle other encodings
10310
            &&
10311
            $offset >= 0 // grapheme_strripos() can't handle negative offset
10312
            &&
10313
            self::$SUPPORT['intl'] === true
10314
        ) {
10315
            $return_tmp = \grapheme_strripos($haystack, $needle, $offset);
10316
            if ($return_tmp !== false) {
10317
                return $return_tmp;
10318
            }
10319
        }
10320
10321
        //
10322
        // fallback for ascii only
10323
        //
10324
10325
        if (ASCII::is_ascii($haystack . $needle)) {
10326
            return \strripos($haystack, $needle, $offset);
10327
        }
10328
10329
        //
10330
        // fallback via vanilla php
10331
        //
10332
10333
        $haystack = self::strtocasefold($haystack, true, false, $encoding);
10334
        $needle = self::strtocasefold($needle, true, false, $encoding);
10335
10336
        return self::strrpos($haystack, $needle, $offset, $encoding, $clean_utf8);
10337
    }
10338
10339
    /**
10340
     * Finds position of last occurrence of a string within another, case-insensitive.
10341
     *
10342
     * @param string $haystack <p>
10343
     *                         The string from which to get the position of the last occurrence
10344
     *                         of needle.
10345
     *                         </p>
10346
     * @param string $needle   <p>
10347
     *                         The string to find in haystack.
10348
     *                         </p>
10349
     * @param int    $offset   [optional] <p>
10350
     *                         The position in haystack
10351
     *                         to start searching.
10352
     *                         </p>
10353
     *
10354
     * @psalm-pure
10355
     *
10356
     * @return false|int
10357
     *                   <p>eturn the numeric position of the last occurrence of needle in the
10358
     *                   haystack string, or false if needle is not found.</p>
10359
     */
10360
    public static function strripos_in_byte(string $haystack, string $needle, int $offset = 0)
10361
    {
10362
        if ($haystack === '' || $needle === '') {
10363
            return false;
10364
        }
10365
10366
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10367
            // "mb_" is available if overload is used, so use it ...
10368
            return \mb_strripos($haystack, $needle, $offset, 'CP850'); // 8-BIT
10369
        }
10370
10371
        return \strripos($haystack, $needle, $offset);
10372
    }
10373
10374
    /**
10375
     * Find the position of the last occurrence of a substring in a string.
10376
     *
10377
     * @see http://php.net/manual/en/function.mb-strrpos.php
10378
     *
10379
     * @param string     $haystack   <p>The string being checked, for the last occurrence of needle</p>
10380
     * @param int|string $needle     <p>The string to find in haystack.<br>Or a code point as int.</p>
10381
     * @param int        $offset     [optional] <p>May be specified to begin searching an arbitrary number of characters
10382
     *                               into the string. Negative values will stop searching at an arbitrary point prior to
10383
     *                               the end of the string.
10384
     *                               </p>
10385
     * @param string     $encoding   [optional] <p>Set the charset.</p>
10386
     * @param bool       $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10387
     *
10388
     * @psalm-pure
10389
     *
10390
     * @return false|int
10391
     *                   <p>The <strong>(int)</strong> numeric position of the last occurrence of needle in the haystack
10392
     *                   string.<br>If needle is not found, it returns false.</p>
10393
     */
10394 35
    public static function strrpos(
10395
        string $haystack,
10396
        $needle,
10397
        int $offset = 0,
10398
        string $encoding = 'UTF-8',
10399
        bool $clean_utf8 = false
10400
    ) {
10401 35
        if ($haystack === '') {
10402 3
            return false;
10403
        }
10404
10405
        // iconv and mbstring do not support integer $needle
10406 34
        if ((int) $needle === $needle && $needle >= 0) {
10407 2
            $needle = (string) self::chr($needle);
10408
        }
10409 34
        $needle = (string) $needle;
10410
10411 34
        if ($needle === '') {
10412 2
            return false;
10413
        }
10414
10415 34
        if ($clean_utf8 === true) {
10416
            // mb_strrpos && iconv_strrpos is not tolerant to invalid characters
10417 4
            $needle = self::clean($needle);
10418 4
            $haystack = self::clean($haystack);
10419
        }
10420
10421 34
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10422 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10423
        }
10424
10425
        //
10426
        // fallback via mbstring
10427
        //
10428
10429 34
        if (self::$SUPPORT['mbstring'] === true) {
10430 34
            if ($encoding === 'UTF-8') {
10431 34
                return \mb_strrpos($haystack, $needle, $offset);
10432
            }
10433
10434 2
            return \mb_strrpos($haystack, $needle, $offset, $encoding);
10435
        }
10436
10437
        //
10438
        // fallback for binary || ascii only
10439
        //
10440
10441
        if (
10442
            $encoding === 'CP850'
10443
            ||
10444
            $encoding === 'ASCII'
10445
        ) {
10446
            return \strrpos($haystack, $needle, $offset);
10447
        }
10448
10449
        if (
10450
            $encoding !== 'UTF-8'
10451
            &&
10452
            self::$SUPPORT['mbstring'] === false
10453
        ) {
10454
            /**
10455
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10456
             */
10457
            \trigger_error('UTF8::strrpos() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10458
        }
10459
10460
        //
10461
        // fallback via intl
10462
        //
10463
10464
        if (
10465
            $offset >= 0 // grapheme_strrpos() can't handle negative offset
10466
            &&
10467
            $encoding === 'UTF-8' // INFO: "grapheme_strrpos()" can't handle other encodings
10468
            &&
10469
            self::$SUPPORT['intl'] === true
10470
        ) {
10471
            $return_tmp = \grapheme_strrpos($haystack, $needle, $offset);
10472
            if ($return_tmp !== false) {
10473
                return $return_tmp;
10474
            }
10475
        }
10476
10477
        //
10478
        // fallback for ascii only
10479
        //
10480
10481
        if (ASCII::is_ascii($haystack . $needle)) {
10482
            return \strrpos($haystack, $needle, $offset);
10483
        }
10484
10485
        //
10486
        // fallback via vanilla php
10487
        //
10488
10489
        $haystack_tmp = null;
10490
        if ($offset > 0) {
10491
            $haystack_tmp = self::substr($haystack, $offset);
10492
        } elseif ($offset < 0) {
10493
            $haystack_tmp = self::substr($haystack, 0, $offset);
10494
            $offset = 0;
10495
        }
10496
10497
        if ($haystack_tmp !== null) {
10498
            if ($haystack_tmp === false) {
10499
                $haystack_tmp = '';
10500
            }
10501
            $haystack = (string) $haystack_tmp;
10502
        }
10503
10504
        $pos = \strrpos($haystack, $needle);
10505
        if ($pos === false) {
10506
            return false;
10507
        }
10508
10509
        /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
10510
        $str_tmp = \substr($haystack, 0, $pos);
10511
        if ($str_tmp === false) {
10512
            return false;
10513
        }
10514
10515
        return $offset + (int) self::strlen($str_tmp);
10516
    }
10517
10518
    /**
10519
     * Find the position of the last occurrence of a substring in a string.
10520
     *
10521
     * @param string $haystack <p>
10522
     *                         The string being checked, for the last occurrence
10523
     *                         of needle.
10524
     *                         </p>
10525
     * @param string $needle   <p>
10526
     *                         The string to find in haystack.
10527
     *                         </p>
10528
     * @param int    $offset   [optional] <p>May be specified to begin searching an arbitrary number of characters into
10529
     *                         the string. Negative values will stop searching at an arbitrary point
10530
     *                         prior to the end of the string.
10531
     *                         </p>
10532
     *
10533
     * @psalm-pure
10534
     *
10535
     * @return false|int
10536
     *                   <p>The numeric position of the last occurrence of needle in the
10537
     *                   haystack string. If needle is not found, it returns false.</p>
10538
     */
10539
    public static function strrpos_in_byte(string $haystack, string $needle, int $offset = 0)
10540
    {
10541
        if ($haystack === '' || $needle === '') {
10542
            return false;
10543
        }
10544
10545
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10546
            // "mb_" is available if overload is used, so use it ...
10547
            return \mb_strrpos($haystack, $needle, $offset, 'CP850'); // 8-BIT
10548
        }
10549
10550
        return \strrpos($haystack, $needle, $offset);
10551
    }
10552
10553
    /**
10554
     * Finds the length of the initial segment of a string consisting entirely of characters contained within a given
10555
     * mask.
10556
     *
10557
     * @param string $str      <p>The input string.</p>
10558
     * @param string $mask     <p>The mask of chars</p>
10559
     * @param int    $offset   [optional]
10560
     * @param int    $length   [optional]
10561
     * @param string $encoding [optional] <p>Set the charset.</p>
10562
     *
10563
     * @psalm-pure
10564
     *
10565
     * @return false|int
10566
     */
10567 10
    public static function strspn(
10568
        string $str,
10569
        string $mask,
10570
        int $offset = 0,
10571
        int $length = null,
10572
        string $encoding = 'UTF-8'
10573
    ) {
10574 10
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10575
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10576
        }
10577
10578 10
        if ($offset || $length !== null) {
10579 2
            if ($encoding === 'UTF-8') {
10580 2
                if ($length === null) {
10581
                    $str = (string) \mb_substr($str, $offset);
10582
                } else {
10583 2
                    $str = (string) \mb_substr($str, $offset, $length);
10584
                }
10585
            } else {
10586
                $str = (string) self::substr($str, $offset, $length, $encoding);
10587
            }
10588
        }
10589
10590 10
        if ($str === '' || $mask === '') {
10591 2
            return 0;
10592
        }
10593
10594 8
        $matches = [];
10595
10596 8
        return \preg_match('/^' . self::rxClass($mask) . '+/u', $str, $matches) ? (int) self::strlen($matches[0], $encoding) : 0;
10597
    }
10598
10599
    /**
10600
     * Returns part of haystack string from the first occurrence of needle to the end of haystack.
10601
     *
10602
     * @param string $haystack      <p>The input string. Must be valid UTF-8.</p>
10603
     * @param string $needle        <p>The string to look for. Must be valid UTF-8.</p>
10604
     * @param bool   $before_needle [optional] <p>
10605
     *                              If <b>TRUE</b>, strstr() returns the part of the
10606
     *                              haystack before the first occurrence of the needle (excluding the needle).
10607
     *                              </p>
10608
     * @param string $encoding      [optional] <p>Set the charset for e.g. "mb_" function</p>
10609
     * @param bool   $clean_utf8    [optional] <p>Remove non UTF-8 chars from the string.</p>
10610
     *
10611
     * @psalm-pure
10612
     *
10613
     * @return false|string
10614
     *                      A sub-string,<br>or <strong>false</strong> if needle is not found
10615
     */
10616 3
    public static function strstr(
10617
        string $haystack,
10618
        string $needle,
10619
        bool $before_needle = false,
10620
        string $encoding = 'UTF-8',
10621
        $clean_utf8 = false
10622
    ) {
10623 3
        if ($haystack === '' || $needle === '') {
10624 2
            return false;
10625
        }
10626
10627 3
        if ($clean_utf8 === true) {
10628
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10629
            // if invalid characters are found in $haystack before $needle
10630
            $needle = self::clean($needle);
10631
            $haystack = self::clean($haystack);
10632
        }
10633
10634 3
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
10635 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
10636
        }
10637
10638
        //
10639
        // fallback via mbstring
10640
        //
10641
10642 3
        if (self::$SUPPORT['mbstring'] === true) {
10643 3
            if ($encoding === 'UTF-8') {
10644 3
                return \mb_strstr($haystack, $needle, $before_needle);
10645
            }
10646
10647 2
            return \mb_strstr($haystack, $needle, $before_needle, $encoding);
10648
        }
10649
10650
        //
10651
        // fallback for binary || ascii only
10652
        //
10653
10654
        if (
10655
            $encoding === 'CP850'
10656
            ||
10657
            $encoding === 'ASCII'
10658
        ) {
10659
            return \strstr($haystack, $needle, $before_needle);
10660
        }
10661
10662
        if (
10663
            $encoding !== 'UTF-8'
10664
            &&
10665
            self::$SUPPORT['mbstring'] === false
10666
        ) {
10667
            /**
10668
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10669
             */
10670
            \trigger_error('UTF8::strstr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
10671
        }
10672
10673
        //
10674
        // fallback via intl
10675
        //
10676
10677
        if (
10678
            $encoding === 'UTF-8' // INFO: "grapheme_strstr()" can't handle other encodings
10679
            &&
10680
            self::$SUPPORT['intl'] === true
10681
        ) {
10682
            $return_tmp = \grapheme_strstr($haystack, $needle, $before_needle);
10683
            if ($return_tmp !== false) {
10684
                return $return_tmp;
10685
            }
10686
        }
10687
10688
        //
10689
        // fallback for ascii only
10690
        //
10691
10692
        if (ASCII::is_ascii($haystack . $needle)) {
10693
            return \strstr($haystack, $needle, $before_needle);
10694
        }
10695
10696
        //
10697
        // fallback via vanilla php
10698
        //
10699
10700
        \preg_match('/^(.*?)' . \preg_quote($needle, '/') . '/us', $haystack, $match);
10701
10702
        if (!isset($match[1])) {
10703
            return false;
10704
        }
10705
10706
        if ($before_needle) {
10707
            return $match[1];
10708
        }
10709
10710
        return self::substr($haystack, (int) self::strlen($match[1]));
10711
    }
10712
10713
    /**
10714
     *  * Finds first occurrence of a string within another.
10715
     *
10716
     * @param string $haystack      <p>
10717
     *                              The string from which to get the first occurrence
10718
     *                              of needle.
10719
     *                              </p>
10720
     * @param string $needle        <p>
10721
     *                              The string to find in haystack.
10722
     *                              </p>
10723
     * @param bool   $before_needle [optional] <p>
10724
     *                              Determines which portion of haystack
10725
     *                              this function returns.
10726
     *                              If set to true, it returns all of haystack
10727
     *                              from the beginning to the first occurrence of needle.
10728
     *                              If set to false, it returns all of haystack
10729
     *                              from the first occurrence of needle to the end,
10730
     *                              </p>
10731
     *
10732
     * @psalm-pure
10733
     *
10734
     * @return false|string
10735
     *                      <p>The portion of haystack,
10736
     *                      or false if needle is not found.</p>
10737
     */
10738
    public static function strstr_in_byte(
10739
        string $haystack,
10740
        string $needle,
10741
        bool $before_needle = false
10742
    ) {
10743
        if ($haystack === '' || $needle === '') {
10744
            return false;
10745
        }
10746
10747
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
10748
            // "mb_" is available if overload is used, so use it ...
10749
            return \mb_strstr($haystack, $needle, $before_needle, 'CP850'); // 8-BIT
10750
        }
10751
10752
        return \strstr($haystack, $needle, $before_needle);
10753
    }
10754
10755
    /**
10756
     * Unicode transformation for case-less matching.
10757
     *
10758
     * @see http://unicode.org/reports/tr21/tr21-5.html
10759
     *
10760
     * @param string      $str        <p>The input string.</p>
10761
     * @param bool        $full       [optional] <p>
10762
     *                                <b>true</b>, replace full case folding chars (default)<br>
10763
     *                                <b>false</b>, use only limited static array [UTF8::$COMMON_CASE_FOLD]
10764
     *                                </p>
10765
     * @param bool        $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
10766
     * @param string      $encoding   [optional] <p>Set the charset.</p>
10767
     * @param string|null $lang       [optional] <p>Set the language for special cases: az, el, lt, tr</p>
10768
     * @param bool        $lower      [optional] <p>Use lowercase string, otherwise use uppercase string. PS: uppercase
10769
     *                                is for some languages better ...</p>
10770
     *
10771
     * @psalm-pure
10772
     *
10773
     * @return string
10774
     */
10775 32
    public static function strtocasefold(
10776
        string $str,
10777
        bool $full = true,
10778
        bool $clean_utf8 = false,
10779
        string $encoding = 'UTF-8',
10780
        string $lang = null,
10781
        $lower = true
10782
    ): string {
10783 32
        if ($str === '') {
10784 5
            return '';
10785
        }
10786
10787 31
        if ($clean_utf8 === true) {
10788
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10789
            // if invalid characters are found in $haystack before $needle
10790 2
            $str = self::clean($str);
10791
        }
10792
10793 31
        $str = self::fixStrCaseHelper($str, $lower, $full);
10794
10795 31
        if ($lang === null && $encoding === 'UTF-8') {
10796 31
            if ($lower === true) {
10797 2
                return \mb_strtolower($str);
10798
            }
10799
10800 29
            return \mb_strtoupper($str);
10801
        }
10802
10803 2
        if ($lower === true) {
10804
            return self::strtolower($str, $encoding, false, $lang);
10805
        }
10806
10807 2
        return self::strtoupper($str, $encoding, false, $lang);
10808
    }
10809
10810
    /**
10811
     * Make a string lowercase.
10812
     *
10813
     * @see http://php.net/manual/en/function.mb-strtolower.php
10814
     *
10815
     * @param string      $str                           <p>The string being lowercased.</p>
10816
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
10817
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
10818
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
10819
     *                                                   tr</p>
10820
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
10821
     *                                                   -> ß</p>
10822
     *
10823
     * @psalm-pure
10824
     *
10825
     * @return string
10826
     *                <p>String with all alphabetic characters converted to lowercase.</p>
10827
     */
10828 73
    public static function strtolower(
10829
        $str,
10830
        string $encoding = 'UTF-8',
10831
        bool $clean_utf8 = false,
10832
        string $lang = null,
10833
        bool $try_to_keep_the_string_length = false
10834
    ): string {
10835
        // init
10836 73
        $str = (string) $str;
10837
10838 73
        if ($str === '') {
10839 1
            return '';
10840
        }
10841
10842 72
        if ($clean_utf8 === true) {
10843
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10844
            // if invalid characters are found in $haystack before $needle
10845 2
            $str = self::clean($str);
10846
        }
10847
10848
        // hack for old php version or for the polyfill ...
10849 72
        if ($try_to_keep_the_string_length === true) {
10850
            $str = self::fixStrCaseHelper($str, true);
10851
        }
10852
10853 72
        if ($lang === null && $encoding === 'UTF-8') {
10854 13
            return \mb_strtolower($str);
10855
        }
10856
10857 61
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10858
10859 61
        if ($lang !== null) {
10860 2
            if (self::$SUPPORT['intl'] === true) {
10861 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
10862
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
10863
                }
10864
10865 2
                $language_code = $lang . '-Lower';
10866 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
10867
                    /**
10868
                     * @psalm-suppress ImpureFunctionCall - is is only a warning
10869
                     */
10870
                    \trigger_error('UTF8::strtolower() cannot handle special language: ' . $lang . ' | supported: ' . \print_r(self::$INTL_TRANSLITERATOR_LIST, true), \E_USER_WARNING);
10871
10872
                    $language_code = 'Any-Lower';
10873
                }
10874
10875
                /** @noinspection PhpComposerExtensionStubsInspection */
10876
                /** @noinspection UnnecessaryCastingInspection */
10877 2
                return (string) \transliterator_transliterate($language_code, $str);
10878
            }
10879
10880
            /**
10881
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10882
             */
10883
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang" parameter: ' . $lang, \E_USER_WARNING);
10884
        }
10885
10886
        // always fallback via symfony polyfill
10887 61
        return \mb_strtolower($str, $encoding);
10888
    }
10889
10890
    /**
10891
     * Make a string uppercase.
10892
     *
10893
     * @see http://php.net/manual/en/function.mb-strtoupper.php
10894
     *
10895
     * @param string      $str                           <p>The string being uppercased.</p>
10896
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
10897
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
10898
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
10899
     *                                                   tr</p>
10900
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
10901
     *                                                   -> ß</p>
10902
     *
10903
     * @psalm-pure
10904
     *
10905
     * @return string
10906
     *                <p>String with all alphabetic characters converted to uppercase.</p>
10907
     */
10908 17
    public static function strtoupper(
10909
        $str,
10910
        string $encoding = 'UTF-8',
10911
        bool $clean_utf8 = false,
10912
        string $lang = null,
10913
        bool $try_to_keep_the_string_length = false
10914
    ): string {
10915
        // init
10916 17
        $str = (string) $str;
10917
10918 17
        if ($str === '') {
10919 1
            return '';
10920
        }
10921
10922 16
        if ($clean_utf8 === true) {
10923
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
10924
            // if invalid characters are found in $haystack before $needle
10925 2
            $str = self::clean($str);
10926
        }
10927
10928
        // hack for old php version or for the polyfill ...
10929 16
        if ($try_to_keep_the_string_length === true) {
10930 2
            $str = self::fixStrCaseHelper($str, false);
10931
        }
10932
10933 16
        if ($lang === null && $encoding === 'UTF-8') {
10934 8
            return \mb_strtoupper($str);
10935
        }
10936
10937 10
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
10938
10939 10
        if ($lang !== null) {
10940 2
            if (self::$SUPPORT['intl'] === true) {
10941 2
                if (self::$INTL_TRANSLITERATOR_LIST === null) {
10942
                    self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
10943
                }
10944
10945 2
                $language_code = $lang . '-Upper';
10946 2
                if (!\in_array($language_code, self::$INTL_TRANSLITERATOR_LIST, true)) {
10947
                    /**
10948
                     * @psalm-suppress ImpureFunctionCall - is is only a warning
10949
                     */
10950
                    \trigger_error('UTF8::strtoupper() without intl for special language: ' . $lang, \E_USER_WARNING);
10951
10952
                    $language_code = 'Any-Upper';
10953
                }
10954
10955
                /** @noinspection PhpComposerExtensionStubsInspection */
10956
                /** @noinspection UnnecessaryCastingInspection */
10957 2
                return (string) \transliterator_transliterate($language_code, $str);
10958
            }
10959
10960
            /**
10961
             * @psalm-suppress ImpureFunctionCall - is is only a warning
10962
             */
10963
            \trigger_error('UTF8::strtolower() without intl cannot handle the "lang"-parameter: ' . $lang, \E_USER_WARNING);
10964
        }
10965
10966
        // always fallback via symfony polyfill
10967 10
        return \mb_strtoupper($str, $encoding);
10968
    }
10969
10970
    /**
10971
     * Translate characters or replace sub-strings.
10972
     *
10973
     * @see http://php.net/manual/en/function.strtr.php
10974
     *
10975
     * @param string          $str  <p>The string being translated.</p>
10976
     * @param string|string[] $from <p>The string replacing from.</p>
10977
     * @param string|string[] $to   [optional] <p>The string being translated to to.</p>
10978
     *
10979
     * @psalm-pure
10980
     *
10981
     * @return string
10982
     *                <p>This function returns a copy of str, translating all occurrences of each character in "from"
10983
     *                to the corresponding character in "to".</p>
10984
     */
10985 2
    public static function strtr(string $str, $from, $to = ''): string
10986
    {
10987 2
        if ($str === '') {
10988
            return '';
10989
        }
10990
10991 2
        if ($from === $to) {
10992
            return $str;
10993
        }
10994
10995 2
        if ($to !== '') {
10996 2
            $from = self::str_split($from);
10997 2
            $to = self::str_split($to);
10998 2
            $count_from = \count($from);
10999 2
            $count_to = \count($to);
11000
11001 2
            if ($count_from > $count_to) {
11002 2
                $from = \array_slice($from, 0, $count_to);
11003 2
            } elseif ($count_from < $count_to) {
11004 2
                $to = \array_slice($to, 0, $count_from);
11005
            }
11006
11007 2
            $from = \array_combine($from, $to);
11008
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
11009 2
            if ($from === false) {
11010
                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) . ')');
11011
            }
11012
        }
11013
11014 2
        if (\is_string($from)) {
11015 2
            return \str_replace($from, '', $str);
11016
        }
11017
11018 2
        return \strtr($str, $from);
11019
    }
11020
11021
    /**
11022
     * Return the width of a string.
11023
     *
11024
     * @param string $str        <p>The input string.</p>
11025
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11026
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11027
     *
11028
     * @psalm-pure
11029
     *
11030
     * @return int
11031
     */
11032 2
    public static function strwidth(
11033
        string $str,
11034
        string $encoding = 'UTF-8',
11035
        bool $clean_utf8 = false
11036
    ): int {
11037 2
        if ($str === '') {
11038 2
            return 0;
11039
        }
11040
11041 2
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
11042 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11043
        }
11044
11045 2
        if ($clean_utf8 === true) {
11046
            // iconv and mbstring are not tolerant to invalid encoding
11047
            // further, their behaviour is inconsistent with that of PHP's substr
11048 2
            $str = self::clean($str);
11049
        }
11050
11051
        //
11052
        // fallback via mbstring
11053
        //
11054
11055 2
        if (self::$SUPPORT['mbstring'] === true) {
11056 2
            if ($encoding === 'UTF-8') {
11057 2
                return \mb_strwidth($str);
11058
            }
11059
11060
            return \mb_strwidth($str, $encoding);
11061
        }
11062
11063
        //
11064
        // fallback via vanilla php
11065
        //
11066
11067
        if ($encoding !== 'UTF-8') {
11068
            $str = self::encode('UTF-8', $str, false, $encoding);
11069
        }
11070
11071
        $wide = 0;
11072
        $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);
11073
11074
        return ($wide << 1) + (int) self::strlen($str, 'UTF-8');
11075
    }
11076
11077
    /**
11078
     * Get part of a string.
11079
     *
11080
     * @see http://php.net/manual/en/function.mb-substr.php
11081
     *
11082
     * @param string $str        <p>The string being checked.</p>
11083
     * @param int    $offset     <p>The first position used in str.</p>
11084
     * @param int    $length     [optional] <p>The maximum length of the returned string.</p>
11085
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11086
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11087
     *
11088
     * @psalm-pure
11089
     *
11090
     * @return false|string
11091
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
11092
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
11093
     *                      characters long, <b>FALSE</b> will be returned.
11094
     */
11095 172
    public static function substr(
11096
        string $str,
11097
        int $offset = 0,
11098
        int $length = null,
11099
        string $encoding = 'UTF-8',
11100
        bool $clean_utf8 = false
11101
    ) {
11102
        // empty string
11103 172
        if ($str === '' || $length === 0) {
11104 8
            return '';
11105
        }
11106
11107 168
        if ($clean_utf8 === true) {
11108
            // iconv and mbstring are not tolerant to invalid encoding
11109
            // further, their behaviour is inconsistent with that of PHP's substr
11110 2
            $str = self::clean($str);
11111
        }
11112
11113
        // whole string
11114 168
        if (!$offset && $length === null) {
11115 7
            return $str;
11116
        }
11117
11118 163
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
11119 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11120
        }
11121
11122
        //
11123
        // fallback via mbstring
11124
        //
11125
11126 163
        if (self::$SUPPORT['mbstring'] === true) {
11127 161
            if ($encoding === 'UTF-8') {
11128 161
                if ($length === null) {
11129 64
                    return \mb_substr($str, $offset);
11130
                }
11131
11132 102
                return \mb_substr($str, $offset, $length);
11133
            }
11134
        }
11135
11136
        //
11137
        // fallback for binary || ascii only
11138
        //
11139
11140
        if (
11141 4
            $encoding === 'CP850'
11142
            ||
11143 4
            $encoding === 'ASCII'
11144
        ) {
11145
            if ($length === null) {
11146
                return \substr($str, $offset);
11147
            }
11148
11149
            return \substr($str, $offset, $length);
11150
        }
11151
11152
        // otherwise we need the string-length
11153 4
        $str_length = 0;
11154 4
        if ($offset || $length === null) {
11155 4
            $str_length = self::strlen($str, $encoding);
11156
        }
11157
11158
        // e.g.: invalid chars + mbstring not installed
11159 4
        if ($str_length === false) {
11160
            return false;
11161
        }
11162
11163
        // empty string
11164 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...
11165
            return '';
11166
        }
11167
11168
        // impossible
11169 4
        if ($offset && $offset > $str_length) {
11170
            return '';
11171
        }
11172
11173 4
        if ($length === null) {
11174 4
            $length = (int) $str_length;
11175
        } else {
11176 2
            $length = (int) $length;
11177
        }
11178
11179
        if (
11180 4
            $encoding !== 'UTF-8'
11181
            &&
11182 4
            self::$SUPPORT['mbstring'] === false
11183
        ) {
11184
            /**
11185
             * @psalm-suppress ImpureFunctionCall - is is only a warning
11186
             */
11187 2
            \trigger_error('UTF8::substr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
11188
        }
11189
11190
        //
11191
        // fallback via intl
11192
        //
11193
11194
        if (
11195 4
            $encoding === 'UTF-8' // INFO: "grapheme_substr()" can't handle other encodings
11196
            &&
11197 4
            $offset >= 0 // grapheme_substr() can't handle negative offset
11198
            &&
11199 4
            self::$SUPPORT['intl'] === true
11200
        ) {
11201
            $return_tmp = \grapheme_substr($str, $offset, $length);
11202
            if ($return_tmp !== false) {
11203
                return $return_tmp;
11204
            }
11205
        }
11206
11207
        //
11208
        // fallback via iconv
11209
        //
11210
11211
        if (
11212 4
            $length >= 0 // "iconv_substr()" can't handle negative length
11213
            &&
11214 4
            self::$SUPPORT['iconv'] === true
11215
        ) {
11216
            $return_tmp = \iconv_substr($str, $offset, $length);
11217
            if ($return_tmp !== false) {
11218
                return $return_tmp;
11219
            }
11220
        }
11221
11222
        //
11223
        // fallback for ascii only
11224
        //
11225
11226 4
        if (ASCII::is_ascii($str)) {
11227
            return \substr($str, $offset, $length);
11228
        }
11229
11230
        //
11231
        // fallback via vanilla php
11232
        //
11233
11234
        // split to array, and remove invalid characters
11235 4
        $array = self::str_split($str);
11236
11237
        // extract relevant part, and join to make sting again
11238 4
        return \implode('', \array_slice($array, $offset, $length));
11239
    }
11240
11241
    /**
11242
     * Binary-safe comparison of two strings from an offset, up to a length of characters.
11243
     *
11244
     * @param string   $str1               <p>The main string being compared.</p>
11245
     * @param string   $str2               <p>The secondary string being compared.</p>
11246
     * @param int      $offset             [optional] <p>The start position for the comparison. If negative, it starts
11247
     *                                     counting from the end of the string.</p>
11248
     * @param int|null $length             [optional] <p>The length of the comparison. The default value is the largest
11249
     *                                     of the length of the str compared to the length of main_str less the
11250
     *                                     offset.</p>
11251
     * @param bool     $case_insensitivity [optional] <p>If case_insensitivity is TRUE, comparison is case
11252
     *                                     insensitive.</p>
11253
     * @param string   $encoding           [optional] <p>Set the charset for e.g. "mb_" function</p>
11254
     *
11255
     * @psalm-pure
11256
     *
11257
     * @return int
11258
     *             <strong>&lt; 0</strong> if str1 is less than str2;<br>
11259
     *             <strong>&gt; 0</strong> if str1 is greater than str2,<br>
11260
     *             <strong>0</strong> if they are equal
11261
     */
11262 2
    public static function substr_compare(
11263
        string $str1,
11264
        string $str2,
11265
        int $offset = 0,
11266
        int $length = null,
11267
        bool $case_insensitivity = false,
11268
        string $encoding = 'UTF-8'
11269
    ): int {
11270
        if (
11271 2
            $offset !== 0
11272
            ||
11273 2
            $length !== null
11274
        ) {
11275 2
            if ($encoding === 'UTF-8') {
11276 2
                if ($length === null) {
11277 2
                    $str1 = (string) \mb_substr($str1, $offset);
11278
                } else {
11279 2
                    $str1 = (string) \mb_substr($str1, $offset, $length);
11280
                }
11281 2
                $str2 = (string) \mb_substr($str2, 0, (int) self::strlen($str1));
11282
            } else {
11283
                $encoding = self::normalize_encoding($encoding, 'UTF-8');
11284
11285
                $str1 = (string) self::substr($str1, $offset, $length, $encoding);
11286
                $str2 = (string) self::substr($str2, 0, (int) self::strlen($str1), $encoding);
11287
            }
11288
        }
11289
11290 2
        if ($case_insensitivity === true) {
11291 2
            return self::strcasecmp($str1, $str2, $encoding);
11292
        }
11293
11294 2
        return self::strcmp($str1, $str2);
11295
    }
11296
11297
    /**
11298
     * Count the number of substring occurrences.
11299
     *
11300
     * @see http://php.net/manual/en/function.substr-count.php
11301
     *
11302
     * @param string $haystack   <p>The string to search in.</p>
11303
     * @param string $needle     <p>The substring to search for.</p>
11304
     * @param int    $offset     [optional] <p>The offset where to start counting.</p>
11305
     * @param int    $length     [optional] <p>
11306
     *                           The maximum length after the specified offset to search for the
11307
     *                           substring. It outputs a warning if the offset plus the length is
11308
     *                           greater than the haystack length.
11309
     *                           </p>
11310
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11311
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11312
     *
11313
     * @psalm-pure
11314
     *
11315
     * @return false|int this functions returns an integer or false if there isn't a string
11316
     */
11317 5
    public static function substr_count(
11318
        string $haystack,
11319
        string $needle,
11320
        int $offset = 0,
11321
        int $length = null,
11322
        string $encoding = 'UTF-8',
11323
        bool $clean_utf8 = false
11324
    ) {
11325 5
        if ($haystack === '' || $needle === '') {
11326 2
            return false;
11327
        }
11328
11329 5
        if ($length === 0) {
11330 2
            return 0;
11331
        }
11332
11333 5
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
11334 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11335
        }
11336
11337 5
        if ($clean_utf8 === true) {
11338
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11339
            // if invalid characters are found in $haystack before $needle
11340
            $needle = self::clean($needle);
11341
            $haystack = self::clean($haystack);
11342
        }
11343
11344 5
        if ($offset || $length > 0) {
11345 2
            if ($length === null) {
11346 2
                $length_tmp = self::strlen($haystack, $encoding);
11347 2
                if ($length_tmp === false) {
11348
                    return false;
11349
                }
11350 2
                $length = (int) $length_tmp;
11351
            }
11352
11353 2
            if ($encoding === 'UTF-8') {
11354 2
                $haystack = (string) \mb_substr($haystack, $offset, $length);
11355
            } else {
11356 2
                $haystack = (string) \mb_substr($haystack, $offset, $length, $encoding);
11357
            }
11358
        }
11359
11360
        if (
11361 5
            $encoding !== 'UTF-8'
11362
            &&
11363 5
            self::$SUPPORT['mbstring'] === false
11364
        ) {
11365
            /**
11366
             * @psalm-suppress ImpureFunctionCall - is is only a warning
11367
             */
11368
            \trigger_error('UTF8::substr_count() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
11369
        }
11370
11371 5
        if (self::$SUPPORT['mbstring'] === true) {
11372 5
            if ($encoding === 'UTF-8') {
11373 5
                return \mb_substr_count($haystack, $needle);
11374
            }
11375
11376 2
            return \mb_substr_count($haystack, $needle, $encoding);
11377
        }
11378
11379
        \preg_match_all('/' . \preg_quote($needle, '/') . '/us', $haystack, $matches, \PREG_SET_ORDER);
11380
11381
        return \count($matches);
11382
    }
11383
11384
    /**
11385
     * Count the number of substring occurrences.
11386
     *
11387
     * @param string $haystack <p>
11388
     *                         The string being checked.
11389
     *                         </p>
11390
     * @param string $needle   <p>
11391
     *                         The string being found.
11392
     *                         </p>
11393
     * @param int    $offset   [optional] <p>
11394
     *                         The offset where to start counting
11395
     *                         </p>
11396
     * @param int    $length   [optional] <p>
11397
     *                         The maximum length after the specified offset to search for the
11398
     *                         substring. It outputs a warning if the offset plus the length is
11399
     *                         greater than the haystack length.
11400
     *                         </p>
11401
     *
11402
     * @psalm-pure
11403
     *
11404
     * @return false|int the number of times the
11405
     *                   needle substring occurs in the
11406
     *                   haystack string
11407
     */
11408
    public static function substr_count_in_byte(
11409
        string $haystack,
11410
        string $needle,
11411
        int $offset = 0,
11412
        int $length = null
11413
    ) {
11414
        if ($haystack === '' || $needle === '') {
11415
            return 0;
11416
        }
11417
11418
        if (
11419
            ($offset || $length !== null)
11420
            &&
11421
            self::$SUPPORT['mbstring_func_overload'] === true
11422
        ) {
11423
            if ($length === null) {
11424
                $length_tmp = self::strlen($haystack);
11425
                if ($length_tmp === false) {
11426
                    return false;
11427
                }
11428
                $length = (int) $length_tmp;
11429
            }
11430
11431
            if (
11432
                (
11433
                    $length !== 0
11434
                    &&
11435
                    $offset !== 0
11436
                )
11437
                &&
11438
                ($length + $offset) <= 0
11439
                &&
11440
                Bootup::is_php('7.1') === false // output from "substr_count()" have changed in PHP 7.1
11441
            ) {
11442
                return false;
11443
            }
11444
11445
            /** @var false|string $haystack_tmp - needed for PhpStan (stubs error) */
11446
            $haystack_tmp = \substr($haystack, $offset, $length);
11447
            if ($haystack_tmp === false) {
11448
                $haystack_tmp = '';
11449
            }
11450
            $haystack = (string) $haystack_tmp;
11451
        }
11452
11453
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
11454
            // "mb_" is available if overload is used, so use it ...
11455
            return \mb_substr_count($haystack, $needle, 'CP850'); // 8-BIT
11456
        }
11457
11458
        if ($length === null) {
11459
            return \substr_count($haystack, $needle, $offset);
11460
        }
11461
11462
        return \substr_count($haystack, $needle, $offset, $length);
11463
    }
11464
11465
    /**
11466
     * Returns the number of occurrences of $substring in the given string.
11467
     * By default, the comparison is case-sensitive, but can be made insensitive
11468
     * by setting $case_sensitive to false.
11469
     *
11470
     * @param string $str            <p>The input string.</p>
11471
     * @param string $substring      <p>The substring to search for.</p>
11472
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
11473
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
11474
     *
11475
     * @psalm-pure
11476
     *
11477
     * @return int
11478
     */
11479 15
    public static function substr_count_simple(
11480
        string $str,
11481
        string $substring,
11482
        bool $case_sensitive = true,
11483
        string $encoding = 'UTF-8'
11484
    ): int {
11485 15
        if ($str === '' || $substring === '') {
11486 2
            return 0;
11487
        }
11488
11489 13
        if ($encoding === 'UTF-8') {
11490 7
            if ($case_sensitive) {
11491
                return (int) \mb_substr_count($str, $substring);
11492
            }
11493
11494 7
            return (int) \mb_substr_count(
11495 7
                \mb_strtoupper($str),
11496 7
                \mb_strtoupper($substring)
11497
            );
11498
        }
11499
11500 6
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
11501
11502 6
        if ($case_sensitive) {
11503 3
            return (int) \mb_substr_count($str, $substring, $encoding);
11504
        }
11505
11506 3
        return (int) \mb_substr_count(
11507 3
            self::strtocasefold($str, true, false, $encoding, null, false),
11508 3
            self::strtocasefold($substring, true, false, $encoding, null, false),
11509 3
            $encoding
11510
        );
11511
    }
11512
11513
    /**
11514
     * Removes a prefix ($needle) from the beginning of the string ($haystack), case-insensitive.
11515
     *
11516
     * @param string $haystack <p>The string to search in.</p>
11517
     * @param string $needle   <p>The substring to search for.</p>
11518
     *
11519
     * @psalm-pure
11520
     *
11521
     * @return string return the sub-string
11522
     */
11523 2
    public static function substr_ileft(string $haystack, string $needle): string
11524
    {
11525 2
        if ($haystack === '') {
11526 2
            return '';
11527
        }
11528
11529 2
        if ($needle === '') {
11530 2
            return $haystack;
11531
        }
11532
11533 2
        if (self::str_istarts_with($haystack, $needle) === true) {
11534 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
11535
        }
11536
11537 2
        return $haystack;
11538
    }
11539
11540
    /**
11541
     * Get part of a string process in bytes.
11542
     *
11543
     * @param string $str    <p>The string being checked.</p>
11544
     * @param int    $offset <p>The first position used in str.</p>
11545
     * @param int    $length [optional] <p>The maximum length of the returned string.</p>
11546
     *
11547
     * @psalm-pure
11548
     *
11549
     * @return false|string
11550
     *                      The portion of <i>str</i> specified by the <i>offset</i> and
11551
     *                      <i>length</i> parameters.</p><p>If <i>str</i> is shorter than <i>offset</i>
11552
     *                      characters long, <b>FALSE</b> will be returned.
11553
     */
11554
    public static function substr_in_byte(string $str, int $offset = 0, int $length = null)
11555
    {
11556
        // empty string
11557
        if ($str === '' || $length === 0) {
11558
            return '';
11559
        }
11560
11561
        // whole string
11562
        if (!$offset && $length === null) {
11563
            return $str;
11564
        }
11565
11566
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
11567
            // "mb_" is available if overload is used, so use it ...
11568
            return \mb_substr($str, $offset, $length, 'CP850'); // 8-BIT
11569
        }
11570
11571
        return \substr($str, $offset, $length ?? 2147483647);
11572
    }
11573
11574
    /**
11575
     * Removes a suffix ($needle) from the end of the string ($haystack), case-insensitive.
11576
     *
11577
     * @param string $haystack <p>The string to search in.</p>
11578
     * @param string $needle   <p>The substring to search for.</p>
11579
     *
11580
     * @psalm-pure
11581
     *
11582
     * @return string return the sub-string
11583
     */
11584 2
    public static function substr_iright(string $haystack, string $needle): string
11585
    {
11586 2
        if ($haystack === '') {
11587 2
            return '';
11588
        }
11589
11590 2
        if ($needle === '') {
11591 2
            return $haystack;
11592
        }
11593
11594 2
        if (self::str_iends_with($haystack, $needle) === true) {
11595 2
            $haystack = (string) \mb_substr($haystack, 0, (int) self::strlen($haystack) - (int) self::strlen($needle));
11596
        }
11597
11598 2
        return $haystack;
11599
    }
11600
11601
    /**
11602
     * Removes a prefix ($needle) from the beginning of the string ($haystack).
11603
     *
11604
     * @param string $haystack <p>The string to search in.</p>
11605
     * @param string $needle   <p>The substring to search for.</p>
11606
     *
11607
     * @psalm-pure
11608
     *
11609
     * @return string return the sub-string
11610
     */
11611 2
    public static function substr_left(string $haystack, string $needle): string
11612
    {
11613 2
        if ($haystack === '') {
11614 2
            return '';
11615
        }
11616
11617 2
        if ($needle === '') {
11618 2
            return $haystack;
11619
        }
11620
11621 2
        if (self::str_starts_with($haystack, $needle) === true) {
11622 2
            $haystack = (string) \mb_substr($haystack, (int) self::strlen($needle));
11623
        }
11624
11625 2
        return $haystack;
11626
    }
11627
11628
    /**
11629
     * Replace text within a portion of a string.
11630
     *
11631
     * source: https://gist.github.com/stemar/8287074
11632
     *
11633
     * @param string|string[] $str         <p>The input string or an array of stings.</p>
11634
     * @param string|string[] $replacement <p>The replacement string or an array of stings.</p>
11635
     * @param int|int[]       $offset      <p>
11636
     *                                     If start is positive, the replacing will begin at the start'th offset
11637
     *                                     into string.
11638
     *                                     <br><br>
11639
     *                                     If start is negative, the replacing will begin at the start'th character
11640
     *                                     from the end of string.
11641
     *                                     </p>
11642
     * @param int|int[]|null  $length      [optional] <p>If given and is positive, it represents the length of the
11643
     *                                     portion of string which is to be replaced. If it is negative, it
11644
     *                                     represents the number of characters from the end of string at which to
11645
     *                                     stop replacing. If it is not given, then it will default to strlen(
11646
     *                                     string ); i.e. end the replacing at the end of string. Of course, if
11647
     *                                     length is zero then this function will have the effect of inserting
11648
     *                                     replacement into string at the given start offset.</p>
11649
     * @param string          $encoding    [optional] <p>Set the charset for e.g. "mb_" function</p>
11650
     *
11651
     * @psalm-pure
11652
     *
11653
     * @return string|string[] The result string is returned. If string is an array then array is returned.
11654
     */
11655 10
    public static function substr_replace(
11656
        $str,
11657
        $replacement,
11658
        $offset,
11659
        $length = null,
11660
        string $encoding = 'UTF-8'
11661
    ) {
11662 10
        if (\is_array($str) === true) {
11663 1
            $num = \count($str);
11664
11665
            // the replacement
11666 1
            if (\is_array($replacement) === true) {
11667 1
                $replacement = \array_slice($replacement, 0, $num);
11668
            } else {
11669 1
                $replacement = \array_pad([$replacement], $num, $replacement);
11670
            }
11671
11672
            // the offset
11673 1
            if (\is_array($offset) === true) {
11674 1
                $offset = \array_slice($offset, 0, $num);
11675 1
                foreach ($offset as &$value_tmp) {
11676 1
                    $value_tmp = (int) $value_tmp === $value_tmp ? $value_tmp : 0;
11677
                }
11678 1
                unset($value_tmp);
11679
            } else {
11680 1
                $offset = \array_pad([$offset], $num, $offset);
11681
            }
11682
11683
            // the length
11684 1
            if ($length === null) {
11685 1
                $length = \array_fill(0, $num, 0);
11686 1
            } elseif (\is_array($length) === true) {
11687 1
                $length = \array_slice($length, 0, $num);
11688 1
                foreach ($length as &$value_tmp_V2) {
11689 1
                    $value_tmp_V2 = (int) $value_tmp_V2 === $value_tmp_V2 ? $value_tmp_V2 : $num;
11690
                }
11691 1
                unset($value_tmp_V2);
11692
            } else {
11693 1
                $length = \array_pad([$length], $num, $length);
11694
            }
11695
11696
            // recursive call
11697 1
            return \array_map([self::class, 'substr_replace'], $str, $replacement, $offset, $length);
11698
        }
11699
11700 10
        if (\is_array($replacement) === true) {
11701 1
            if ($replacement !== []) {
11702 1
                $replacement = $replacement[0];
11703
            } else {
11704 1
                $replacement = '';
11705
            }
11706
        }
11707
11708
        // init
11709 10
        $str = (string) $str;
11710 10
        $replacement = (string) $replacement;
11711
11712 10
        if (\is_array($length) === true) {
11713
            throw new \InvalidArgumentException('Parameter "$length" can only be an array, if "$str" is also an array.');
11714
        }
11715
11716 10
        if (\is_array($offset) === true) {
11717
            throw new \InvalidArgumentException('Parameter "$offset" can only be an array, if "$str" is also an array.');
11718
        }
11719
11720 10
        if ($str === '') {
11721 1
            return $replacement;
11722
        }
11723
11724 9
        if (self::$SUPPORT['mbstring'] === true) {
11725 9
            $string_length = (int) self::strlen($str, $encoding);
11726
11727 9
            if ($offset < 0) {
11728 1
                $offset = (int) \max(0, $string_length + $offset);
11729 9
            } elseif ($offset > $string_length) {
11730 1
                $offset = $string_length;
11731
            }
11732
11733 9
            if ($length !== null && $length < 0) {
11734 1
                $length = (int) \max(0, $string_length - $offset + $length);
11735 9
            } elseif ($length === null || $length > $string_length) {
11736 4
                $length = $string_length;
11737
            }
11738
11739
            /** @noinspection AdditionOperationOnArraysInspection */
11740 9
            if (($offset + $length) > $string_length) {
11741 4
                $length = $string_length - $offset;
11742
            }
11743
11744
            /** @noinspection AdditionOperationOnArraysInspection */
11745 9
            return ((string) \mb_substr($str, 0, $offset, $encoding)) .
11746 9
                   $replacement .
11747 9
                   ((string) \mb_substr($str, $offset + $length, $string_length - $offset - $length, $encoding));
11748
        }
11749
11750
        //
11751
        // fallback for ascii only
11752
        //
11753
11754
        if (ASCII::is_ascii($str)) {
11755
            return ($length === null) ?
11756
                \substr_replace($str, $replacement, $offset) :
11757
                \substr_replace($str, $replacement, $offset, $length);
11758
        }
11759
11760
        //
11761
        // fallback via vanilla php
11762
        //
11763
11764
        \preg_match_all('/./us', $str, $str_matches);
11765
        \preg_match_all('/./us', $replacement, $replacement_matches);
11766
11767
        if ($length === null) {
11768
            $length_tmp = self::strlen($str, $encoding);
11769
            if ($length_tmp === false) {
11770
                // e.g.: non mbstring support + invalid chars
11771
                return '';
11772
            }
11773
            $length = (int) $length_tmp;
11774
        }
11775
11776
        \array_splice($str_matches[0], $offset, $length, $replacement_matches[0]);
11777
11778
        return \implode('', $str_matches[0]);
11779
    }
11780
11781
    /**
11782
     * Removes a suffix ($needle) from the end of the string ($haystack).
11783
     *
11784
     * @param string $haystack <p>The string to search in.</p>
11785
     * @param string $needle   <p>The substring to search for.</p>
11786
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
11787
     *
11788
     * @psalm-pure
11789
     *
11790
     * @return string return the sub-string
11791
     */
11792 2
    public static function substr_right(
11793
        string $haystack,
11794
        string $needle,
11795
        string $encoding = 'UTF-8'
11796
    ): string {
11797 2
        if ($haystack === '') {
11798 2
            return '';
11799
        }
11800
11801 2
        if ($needle === '') {
11802 2
            return $haystack;
11803
        }
11804
11805
        if (
11806 2
            $encoding === 'UTF-8'
11807
            &&
11808 2
            \substr($haystack, -\strlen($needle)) === $needle
11809
        ) {
11810 2
            return (string) \mb_substr($haystack, 0, (int) \mb_strlen($haystack) - (int) \mb_strlen($needle));
11811
        }
11812
11813 2
        if (\substr($haystack, -\strlen($needle)) === $needle) {
11814
            return (string) self::substr(
11815
                $haystack,
11816
                0,
11817
                (int) self::strlen($haystack, $encoding) - (int) self::strlen($needle, $encoding),
11818
                $encoding
11819
            );
11820
        }
11821
11822 2
        return $haystack;
11823
    }
11824
11825
    /**
11826
     * Returns a case swapped version of the string.
11827
     *
11828
     * @param string $str        <p>The input string.</p>
11829
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
11830
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
11831
     *
11832
     * @psalm-pure
11833
     *
11834
     * @return string each character's case swapped
11835
     */
11836 6
    public static function swapCase(string $str, string $encoding = 'UTF-8', bool $clean_utf8 = false): string
11837
    {
11838 6
        if ($str === '') {
11839 1
            return '';
11840
        }
11841
11842 6
        if ($clean_utf8 === true) {
11843
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11844
            // if invalid characters are found in $haystack before $needle
11845 2
            $str = self::clean($str);
11846
        }
11847
11848 6
        if ($encoding === 'UTF-8') {
11849 4
            return (string) (\mb_strtolower($str) ^ \mb_strtoupper($str) ^ $str);
11850
        }
11851
11852 4
        return (string) (self::strtolower($str, $encoding) ^ self::strtoupper($str, $encoding) ^ $str);
11853
    }
11854
11855
    /**
11856
     * Checks whether symfony-polyfills are used.
11857
     *
11858
     * @psalm-pure
11859
     *
11860
     * @return bool
11861
     *              <strong>true</strong> if in use, <strong>false</strong> otherwise
11862
     */
11863
    public static function symfony_polyfill_used(): bool
11864
    {
11865
        // init
11866
        $return = false;
11867
11868
        $return_tmp = \extension_loaded('mbstring');
11869
        if ($return_tmp === false && \function_exists('mb_strlen')) {
11870
            $return = true;
11871
        }
11872
11873
        $return_tmp = \extension_loaded('iconv');
11874
        if ($return_tmp === false && \function_exists('iconv')) {
11875
            $return = true;
11876
        }
11877
11878
        return $return;
11879
    }
11880
11881
    /**
11882
     * @param string $str
11883
     * @param int    $tab_length
11884
     *
11885
     * @psalm-pure
11886
     *
11887
     * @return string
11888
     */
11889 6
    public static function tabs_to_spaces(string $str, int $tab_length = 4): string
11890
    {
11891 6
        if ($tab_length === 4) {
11892 3
            $spaces = '    ';
11893 3
        } elseif ($tab_length === 2) {
11894 1
            $spaces = '  ';
11895
        } else {
11896 2
            $spaces = \str_repeat(' ', $tab_length);
11897
        }
11898
11899 6
        return \str_replace("\t", $spaces, $str);
11900
    }
11901
11902
    /**
11903
     * Converts the first character of each word in the string to uppercase
11904
     * and all other chars to lowercase.
11905
     *
11906
     * @param string      $str                           <p>The input string.</p>
11907
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
11908
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
11909
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
11910
     *                                                   tr</p>
11911
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
11912
     *                                                   -> ß</p>
11913
     *
11914
     * @psalm-pure
11915
     *
11916
     * @return string
11917
     *                <p>A string with all characters of $str being title-cased.</p>
11918
     */
11919 5
    public static function titlecase(
11920
        string $str,
11921
        string $encoding = 'UTF-8',
11922
        bool $clean_utf8 = false,
11923
        string $lang = null,
11924
        bool $try_to_keep_the_string_length = false
11925
    ): string {
11926 5
        if ($clean_utf8 === true) {
11927
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
11928
            // if invalid characters are found in $haystack before $needle
11929
            $str = self::clean($str);
11930
        }
11931
11932
        if (
11933 5
            $lang === null
11934
            &&
11935 5
            $try_to_keep_the_string_length === false
11936
        ) {
11937 5
            if ($encoding === 'UTF-8') {
11938 3
                return \mb_convert_case($str, \MB_CASE_TITLE);
11939
            }
11940
11941 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
11942
11943 2
            return \mb_convert_case($str, \MB_CASE_TITLE, $encoding);
11944
        }
11945
11946
        return self::str_titleize(
11947
            $str,
11948
            null,
11949
            $encoding,
11950
            false,
11951
            $lang,
11952
            $try_to_keep_the_string_length,
11953
            false
11954
        );
11955
    }
11956
11957
    /**
11958
     * alias for "UTF8::to_ascii()"
11959
     *
11960
     * @param string $str
11961
     * @param string $subst_chr
11962
     * @param bool   $strict
11963
     *
11964
     * @psalm-pure
11965
     *
11966
     * @return string
11967
     *
11968
     * @see        UTF8::to_ascii()
11969
     * @deprecated <p>please use "UTF8::to_ascii()"</p>
11970
     */
11971 7
    public static function toAscii(
11972
        string $str,
11973
        string $subst_chr = '?',
11974
        bool $strict = false
11975
    ): string {
11976 7
        return self::to_ascii($str, $subst_chr, $strict);
11977
    }
11978
11979
    /**
11980
     * alias for "UTF8::to_iso8859()"
11981
     *
11982
     * @param string|string[] $str
11983
     *
11984
     * @psalm-pure
11985
     *
11986
     * @return string|string[]
11987
     *
11988
     * @see        UTF8::to_iso8859()
11989
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
11990
     */
11991 2
    public static function toIso8859($str)
11992
    {
11993 2
        return self::to_iso8859($str);
11994
    }
11995
11996
    /**
11997
     * alias for "UTF8::to_latin1()"
11998
     *
11999
     * @param string|string[] $str
12000
     *
12001
     * @psalm-pure
12002
     *
12003
     * @return string|string[]
12004
     *
12005
     * @see        UTF8::to_iso8859()
12006
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
12007
     */
12008 2
    public static function toLatin1($str)
12009
    {
12010 2
        return self::to_iso8859($str);
12011
    }
12012
12013
    /**
12014
     * alias for "UTF8::to_utf8()"
12015
     *
12016
     * @param string|string[] $str
12017
     *
12018
     * @psalm-pure
12019
     *
12020
     * @return string|string[]
12021
     *
12022
     * @see        UTF8::to_utf8()
12023
     * @deprecated <p>please use "UTF8::to_utf8()"</p>
12024
     */
12025 2
    public static function toUTF8($str)
12026
    {
12027 2
        return self::to_utf8($str);
12028
    }
12029
12030
    /**
12031
     * Convert a string into ASCII.
12032
     *
12033
     * @param string $str     <p>The input string.</p>
12034
     * @param string $unknown [optional] <p>Character use if character unknown. (default is ?)</p>
12035
     * @param bool   $strict  [optional] <p>Use "transliterator_transliterate()" from PHP-Intl | WARNING: bad
12036
     *                        performance</p>
12037
     *
12038
     * @psalm-pure
12039
     *
12040
     * @return string
12041
     */
12042 37
    public static function to_ascii(
12043
        string $str,
12044
        string $unknown = '?',
12045
        bool $strict = false
12046
    ): string {
12047 37
        return ASCII::to_transliterate($str, $unknown, $strict);
12048
    }
12049
12050
    /**
12051
     * @param mixed $str
12052
     *
12053
     * @psalm-pure
12054
     *
12055
     * @return bool
12056
     */
12057 19
    public static function to_boolean($str): bool
12058
    {
12059
        // init
12060 19
        $str = (string) $str;
12061
12062 19
        if ($str === '') {
12063 2
            return false;
12064
        }
12065
12066
        // Info: http://php.net/manual/en/filter.filters.validate.php
12067
        $map = [
12068 17
            'true'  => true,
12069
            '1'     => true,
12070
            'on'    => true,
12071
            'yes'   => true,
12072
            'false' => false,
12073
            '0'     => false,
12074
            'off'   => false,
12075
            'no'    => false,
12076
        ];
12077
12078 17
        if (isset($map[$str])) {
12079 11
            return $map[$str];
12080
        }
12081
12082 6
        $key = \strtolower($str);
12083 6
        if (isset($map[$key])) {
12084 2
            return $map[$key];
12085
        }
12086
12087 4
        if (\is_numeric($str)) {
12088 2
            return ((float) $str + 0) > 0;
12089
        }
12090
12091 2
        return (bool) \trim($str);
12092
    }
12093
12094
    /**
12095
     * Convert given string to safe filename (and keep string case).
12096
     *
12097
     * @param string $str
12098
     * @param bool   $use_transliterate No transliteration, conversion etc. is done by default - unsafe characters are
12099
     *                                  simply replaced with hyphen.
12100
     * @param string $fallback_char
12101
     *
12102
     * @psalm-pure
12103
     *
12104
     * @return string
12105
     */
12106 1
    public static function to_filename(
12107
        string $str,
12108
        bool $use_transliterate = false,
12109
        string $fallback_char = '-'
12110
    ): string {
12111 1
        return ASCII::to_filename(
12112 1
            $str,
12113 1
            $use_transliterate,
12114 1
            $fallback_char
12115
        );
12116
    }
12117
12118
    /**
12119
     * Convert a string into "ISO-8859"-encoding (Latin-1).
12120
     *
12121
     * @param string|string[] $str
12122
     *
12123
     * @psalm-pure
12124
     *
12125
     * @return string|string[]
12126
     */
12127 8
    public static function to_iso8859($str)
12128
    {
12129 8
        if (\is_array($str) === true) {
12130 2
            foreach ($str as $k => &$v) {
12131 2
                $v = self::to_iso8859($v);
12132
            }
12133
12134 2
            return $str;
12135
        }
12136
12137 8
        $str = (string) $str;
12138 8
        if ($str === '') {
12139 2
            return '';
12140
        }
12141
12142 8
        return self::utf8_decode($str);
12143
    }
12144
12145
    /**
12146
     * alias for "UTF8::to_iso8859()"
12147
     *
12148
     * @param string|string[] $str
12149
     *
12150
     * @psalm-pure
12151
     *
12152
     * @return string|string[]
12153
     *
12154
     * @see        UTF8::to_iso8859()
12155
     * @deprecated <p>please use "UTF8::to_iso8859()"</p>
12156
     */
12157 2
    public static function to_latin1($str)
12158
    {
12159 2
        return self::to_iso8859($str);
12160
    }
12161
12162
    /**
12163
     * This function leaves UTF-8 characters alone, while converting almost all non-UTF8 to UTF8.
12164
     *
12165
     * <ul>
12166
     * <li>It decode UTF-8 codepoints and Unicode escape sequences.</li>
12167
     * <li>It assumes that the encoding of the original string is either WINDOWS-1252 or ISO-8859.</li>
12168
     * <li>WARNING: It does not remove invalid UTF-8 characters, so you maybe need to use "UTF8::clean()" for this
12169
     * case.</li>
12170
     * </ul>
12171
     *
12172
     * @param string|string[] $str                        <p>Any string or array.</p>
12173
     * @param bool            $decode_html_entity_to_utf8 <p>Set to true, if you need to decode html-entities.</p>
12174
     *
12175
     * @psalm-pure
12176
     *
12177
     * @return string|string[] the UTF-8 encoded string
12178
     */
12179 43
    public static function to_utf8($str, bool $decode_html_entity_to_utf8 = false)
12180
    {
12181 43
        if (\is_array($str) === true) {
12182 4
            foreach ($str as $k => &$v) {
12183 4
                $v = self::to_utf8($v, $decode_html_entity_to_utf8);
12184
            }
12185
12186 4
            return $str;
12187
        }
12188
12189 43
        $str = (string) $str;
12190 43
        if ($str === '') {
12191 7
            return $str;
12192
        }
12193
12194 43
        $max = \strlen($str);
12195 43
        $buf = '';
12196
12197 43
        for ($i = 0; $i < $max; ++$i) {
12198 43
            $c1 = $str[$i];
12199
12200 43
            if ($c1 >= "\xC0") { // should be converted to UTF8, if it's not UTF8 already
12201
12202 39
                if ($c1 <= "\xDF") { // looks like 2 bytes UTF8
12203
12204 36
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
12205
12206 36
                    if ($c2 >= "\x80" && $c2 <= "\xBF") { // yeah, almost sure it's UTF8 already
12207 22
                        $buf .= $c1 . $c2;
12208 22
                        ++$i;
12209
                    } else { // not valid UTF8 - convert it
12210 36
                        $buf .= self::to_utf8_convert_helper($c1);
12211
                    }
12212 36
                } elseif ($c1 >= "\xE0" && $c1 <= "\xEF") { // looks like 3 bytes UTF8
12213
12214 35
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
12215 35
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
12216
12217 35
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF") { // yeah, almost sure it's UTF8 already
12218 17
                        $buf .= $c1 . $c2 . $c3;
12219 17
                        $i += 2;
12220
                    } else { // not valid UTF8 - convert it
12221 35
                        $buf .= self::to_utf8_convert_helper($c1);
12222
                    }
12223 28
                } elseif ($c1 >= "\xF0" && $c1 <= "\xF7") { // looks like 4 bytes UTF8
12224
12225 28
                    $c2 = $i + 1 >= $max ? "\x00" : $str[$i + 1];
12226 28
                    $c3 = $i + 2 >= $max ? "\x00" : $str[$i + 2];
12227 28
                    $c4 = $i + 3 >= $max ? "\x00" : $str[$i + 3];
12228
12229 28
                    if ($c2 >= "\x80" && $c2 <= "\xBF" && $c3 >= "\x80" && $c3 <= "\xBF" && $c4 >= "\x80" && $c4 <= "\xBF") { // yeah, almost sure it's UTF8 already
12230 10
                        $buf .= $c1 . $c2 . $c3 . $c4;
12231 10
                        $i += 3;
12232
                    } else { // not valid UTF8 - convert it
12233 28
                        $buf .= self::to_utf8_convert_helper($c1);
12234
                    }
12235
                } else { // doesn't look like UTF8, but should be converted
12236
12237 39
                    $buf .= self::to_utf8_convert_helper($c1);
12238
                }
12239 40
            } elseif (($c1 & "\xC0") === "\x80") { // needs conversion
12240
12241 4
                $buf .= self::to_utf8_convert_helper($c1);
12242
            } else { // it doesn't need conversion
12243
12244 40
                $buf .= $c1;
12245
            }
12246
        }
12247
12248
        // decode unicode escape sequences + unicode surrogate pairs
12249 43
        $buf = \preg_replace_callback(
12250 43
            '/\\\\u([dD][89abAB][0-9a-fA-F]{2})\\\\u([dD][cdefCDEF][\da-fA-F]{2})|\\\\u([0-9a-fA-F]{4})/',
12251
            /**
12252
             * @param array $matches
12253
             *
12254
             * @psalm-pure
12255
             *
12256
             * @return string
12257
             */
12258
            static function (array $matches): string {
12259 13
                if (isset($matches[3])) {
12260 13
                    $cp = (int) \hexdec($matches[3]);
12261
                } else {
12262
                    // http://unicode.org/faq/utf_bom.html#utf16-4
12263 1
                    $cp = ((int) \hexdec($matches[1]) << 10)
12264 1
                          + (int) \hexdec($matches[2])
12265 1
                          + 0x10000
12266 1
                          - (0xD800 << 10)
12267 1
                          - 0xDC00;
12268
                }
12269
12270
                // https://github.com/php/php-src/blob/php-7.3.2/ext/standard/html.c#L471
12271
                //
12272
                // php_utf32_utf8(unsigned char *buf, unsigned k)
12273
12274 13
                if ($cp < 0x80) {
12275 8
                    return (string) self::chr($cp);
12276
                }
12277
12278 10
                if ($cp < 0xA0) {
12279
                    /** @noinspection UnnecessaryCastingInspection */
12280
                    return (string) self::chr(0xC0 | $cp >> 6) . (string) self::chr(0x80 | $cp & 0x3F);
12281
                }
12282
12283 10
                return self::decimal_to_chr($cp);
12284 43
            },
12285 43
            $buf
12286
        );
12287
12288 43
        if ($buf === null) {
12289
            return '';
12290
        }
12291
12292
        // decode UTF-8 codepoints
12293 43
        if ($decode_html_entity_to_utf8 === true) {
12294 3
            $buf = self::html_entity_decode($buf);
12295
        }
12296
12297 43
        return $buf;
12298
    }
12299
12300
    /**
12301
     * Returns the given string as an integer, or null if the string isn't numeric.
12302
     *
12303
     * @param string $str
12304
     *
12305
     * @psalm-pure
12306
     *
12307
     * @return int|null
12308
     *                  <p>null if the string isn't numeric</p>
12309
     */
12310 1
    public static function to_int(string $str)
12311
    {
12312 1
        if (\is_numeric($str)) {
12313 1
            return (int) $str;
12314
        }
12315
12316 1
        return null;
12317
    }
12318
12319
    /**
12320
     * Returns the given input as string, or null if the input isn't int|float|string
12321
     * and do not implement the "__toString()" method.
12322
     *
12323
     * @param mixed $input
12324
     *
12325
     * @psalm-pure
12326
     *
12327
     * @return string|null
12328
     *                     <p>null if the input isn't int|float|string and has no "__toString()" method</p>
12329
     */
12330 1
    public static function to_string($input)
12331
    {
12332
        /** @var string $input_type - hack for psalm */
12333 1
        $input_type = \gettype($input);
12334
12335
        if (
12336 1
            $input_type === 'string'
12337
            ||
12338 1
            $input_type === 'integer'
12339
            ||
12340 1
            $input_type === 'float'
12341
            ||
12342 1
            $input_type === 'double'
12343
        ) {
12344 1
            return (string) $input;
12345
        }
12346
12347
        if (
12348 1
            $input_type === 'object'
12349
            &&
12350 1
            \method_exists($input, '__toString')
12351
        ) {
12352 1
            return (string) $input;
12353
        }
12354
12355 1
        return null;
12356
    }
12357
12358
    /**
12359
     * Strip whitespace or other characters from the beginning and end of a UTF-8 string.
12360
     *
12361
     * INFO: This is slower then "trim()"
12362
     *
12363
     * We can only use the original-function, if we use <= 7-Bit in the string / chars
12364
     * but the check for ASCII (7-Bit) cost more time, then we can safe here.
12365
     *
12366
     * @param string      $str   <p>The string to be trimmed</p>
12367
     * @param string|null $chars [optional] <p>Optional characters to be stripped</p>
12368
     *
12369
     * @psalm-pure
12370
     *
12371
     * @return string the trimmed string
12372
     */
12373 56
    public static function trim(string $str = '', string $chars = null): string
12374
    {
12375 56
        if ($str === '') {
12376 9
            return '';
12377
        }
12378
12379 49
        if (self::$SUPPORT['mbstring'] === true) {
12380 49
            if ($chars) {
12381
                /** @noinspection PregQuoteUsageInspection */
12382 27
                $chars = \preg_quote($chars);
12383 27
                $pattern = "^[${chars}]+|[${chars}]+\$";
12384
            } else {
12385 22
                $pattern = '^[\\s]+|[\\s]+$';
12386
            }
12387
12388
            /** @noinspection PhpComposerExtensionStubsInspection */
12389 49
            return (string) \mb_ereg_replace($pattern, '', $str);
12390
        }
12391
12392 8
        if ($chars) {
12393
            $chars = \preg_quote($chars, '/');
12394
            $pattern = "^[${chars}]+|[${chars}]+\$";
12395
        } else {
12396 8
            $pattern = '^[\\s]+|[\\s]+$';
12397
        }
12398
12399 8
        return self::regex_replace($str, $pattern, '', '', '/');
12400
    }
12401
12402
    /**
12403
     * Makes string's first char uppercase.
12404
     *
12405
     * @param string      $str                           <p>The input string.</p>
12406
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
12407
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
12408
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
12409
     *                                                   tr</p>
12410
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
12411
     *                                                   -> ß</p>
12412
     *
12413
     * @psalm-pure
12414
     *
12415
     * @return string the resulting string
12416
     */
12417 69
    public static function ucfirst(
12418
        string $str,
12419
        string $encoding = 'UTF-8',
12420
        bool $clean_utf8 = false,
12421
        string $lang = null,
12422
        bool $try_to_keep_the_string_length = false
12423
    ): string {
12424 69
        if ($str === '') {
12425 3
            return '';
12426
        }
12427
12428 68
        if ($clean_utf8 === true) {
12429
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
12430
            // if invalid characters are found in $haystack before $needle
12431 1
            $str = self::clean($str);
12432
        }
12433
12434 68
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
12435
12436 68
        if ($encoding === 'UTF-8') {
12437 22
            $str_part_two = (string) \mb_substr($str, 1);
12438
12439 22
            if ($use_mb_functions === true) {
12440 22
                $str_part_one = \mb_strtoupper(
12441 22
                    (string) \mb_substr($str, 0, 1)
12442
                );
12443
            } else {
12444
                $str_part_one = self::strtoupper(
12445
                    (string) \mb_substr($str, 0, 1),
12446
                    $encoding,
12447
                    false,
12448
                    $lang,
12449 22
                    $try_to_keep_the_string_length
12450
                );
12451
            }
12452
        } else {
12453 47
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
12454
12455 47
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
12456
12457 47
            if ($use_mb_functions === true) {
12458 47
                $str_part_one = \mb_strtoupper(
12459 47
                    (string) \mb_substr($str, 0, 1, $encoding),
12460 47
                    $encoding
12461
                );
12462
            } else {
12463
                $str_part_one = self::strtoupper(
12464
                    (string) self::substr($str, 0, 1, $encoding),
12465
                    $encoding,
12466
                    false,
12467
                    $lang,
12468
                    $try_to_keep_the_string_length
12469
                );
12470
            }
12471
        }
12472
12473 68
        return $str_part_one . $str_part_two;
12474
    }
12475
12476
    /**
12477
     * alias for "UTF8::ucfirst()"
12478
     *
12479
     * @param string $str
12480
     * @param string $encoding
12481
     * @param bool   $clean_utf8
12482
     *
12483
     * @psalm-pure
12484
     *
12485
     * @return string
12486
     *
12487
     * @see        UTF8::ucfirst()
12488
     * @deprecated <p>please use "UTF8::ucfirst()"</p>
12489
     */
12490 1
    public static function ucword(
12491
        string $str,
12492
        string $encoding = 'UTF-8',
12493
        bool $clean_utf8 = false
12494
    ): string {
12495 1
        return self::ucfirst($str, $encoding, $clean_utf8);
12496
    }
12497
12498
    /**
12499
     * Uppercase for all words in the string.
12500
     *
12501
     * @param string   $str        <p>The input string.</p>
12502
     * @param string[] $exceptions [optional] <p>Exclusion for some words.</p>
12503
     * @param string   $char_list  [optional] <p>Additional chars that contains to words and do not start a new
12504
     *                             word.</p>
12505
     * @param string   $encoding   [optional] <p>Set the charset.</p>
12506
     * @param bool     $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
12507
     *
12508
     * @psalm-pure
12509
     *
12510
     * @return string
12511
     */
12512 8
    public static function ucwords(
12513
        string $str,
12514
        array $exceptions = [],
12515
        string $char_list = '',
12516
        string $encoding = 'UTF-8',
12517
        bool $clean_utf8 = false
12518
    ): string {
12519 8
        if (!$str) {
12520 2
            return '';
12521
        }
12522
12523
        // INFO: mb_convert_case($str, MB_CASE_TITLE);
12524
        // -> MB_CASE_TITLE didn't only uppercase the first letter, it also lowercase all other letters
12525
12526 7
        if ($clean_utf8 === true) {
12527
            // "mb_strpos()" and "iconv_strpos()" returns wrong position,
12528
            // if invalid characters are found in $haystack before $needle
12529 1
            $str = self::clean($str);
12530
        }
12531
12532 7
        $use_php_default_functions = !(bool) ($char_list . \implode('', $exceptions));
12533
12534
        if (
12535 7
            $use_php_default_functions === true
12536
            &&
12537 7
            ASCII::is_ascii($str) === true
12538
        ) {
12539
            return \ucwords($str);
12540
        }
12541
12542 7
        $words = self::str_to_words($str, $char_list);
12543 7
        $use_exceptions = $exceptions !== [];
12544
12545 7
        $words_str = '';
12546 7
        foreach ($words as &$word) {
12547 7
            if (!$word) {
12548 7
                continue;
12549
            }
12550
12551
            if (
12552 7
                $use_exceptions === false
12553
                ||
12554 7
                !\in_array($word, $exceptions, true)
12555
            ) {
12556 7
                $words_str .= self::ucfirst($word, $encoding);
12557
            } else {
12558 7
                $words_str .= $word;
12559
            }
12560
        }
12561
12562 7
        return $words_str;
12563
    }
12564
12565
    /**
12566
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
12567
     *
12568
     * e.g:
12569
     * 'test+test'                     => 'test test'
12570
     * 'D&#252;sseldorf'               => 'Düsseldorf'
12571
     * 'D%FCsseldorf'                  => 'Düsseldorf'
12572
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
12573
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
12574
     * 'Düsseldorf'                   => 'Düsseldorf'
12575
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
12576
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
12577
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
12578
     *
12579
     * @param string $str          <p>The input string.</p>
12580
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
12581
     *
12582
     * @psalm-pure
12583
     *
12584
     * @return string
12585
     */
12586 4
    public static function urldecode(string $str, bool $multi_decode = true): string
12587
    {
12588 4
        if ($str === '') {
12589 3
            return '';
12590
        }
12591
12592
        if (
12593 4
            \strpos($str, '&') === false
12594
            &&
12595 4
            \strpos($str, '%') === false
12596
            &&
12597 4
            \strpos($str, '+') === false
12598
            &&
12599 4
            \strpos($str, '\u') === false
12600
        ) {
12601 3
            return self::fix_simple_utf8($str);
12602
        }
12603
12604 4
        $str = self::urldecode_unicode_helper($str);
12605
12606 4
        if ($multi_decode === true) {
12607
            do {
12608 3
                $str_compare = $str;
12609
12610
                /**
12611
                 * @psalm-suppress PossiblyInvalidArgument
12612
                 */
12613 3
                $str = self::fix_simple_utf8(
12614 3
                    \urldecode(
12615 3
                        self::html_entity_decode(
12616 3
                            self::to_utf8($str),
12617 3
                            \ENT_QUOTES | \ENT_HTML5
12618
                        )
12619
                    )
12620
                );
12621 3
            } while ($str_compare !== $str);
12622
        } else {
12623
            /**
12624
             * @psalm-suppress PossiblyInvalidArgument
12625
             */
12626 1
            $str = self::fix_simple_utf8(
12627 1
                \urldecode(
12628 1
                    self::html_entity_decode(
12629 1
                        self::to_utf8($str),
12630 1
                        \ENT_QUOTES | \ENT_HTML5
12631
                    )
12632
                )
12633
            );
12634
        }
12635
12636 4
        return $str;
12637
    }
12638
12639
    /**
12640
     * Return a array with "urlencoded"-win1252 -> UTF-8
12641
     *
12642
     * @psalm-pure
12643
     *
12644
     * @return string[]
12645
     *
12646
     * @deprecated <p>please use the "UTF8::urldecode()" function to decode a string</p>
12647
     */
12648 2
    public static function urldecode_fix_win1252_chars(): array
12649
    {
12650
        return [
12651 2
            '%20' => ' ',
12652
            '%21' => '!',
12653
            '%22' => '"',
12654
            '%23' => '#',
12655
            '%24' => '$',
12656
            '%25' => '%',
12657
            '%26' => '&',
12658
            '%27' => "'",
12659
            '%28' => '(',
12660
            '%29' => ')',
12661
            '%2A' => '*',
12662
            '%2B' => '+',
12663
            '%2C' => ',',
12664
            '%2D' => '-',
12665
            '%2E' => '.',
12666
            '%2F' => '/',
12667
            '%30' => '0',
12668
            '%31' => '1',
12669
            '%32' => '2',
12670
            '%33' => '3',
12671
            '%34' => '4',
12672
            '%35' => '5',
12673
            '%36' => '6',
12674
            '%37' => '7',
12675
            '%38' => '8',
12676
            '%39' => '9',
12677
            '%3A' => ':',
12678
            '%3B' => ';',
12679
            '%3C' => '<',
12680
            '%3D' => '=',
12681
            '%3E' => '>',
12682
            '%3F' => '?',
12683
            '%40' => '@',
12684
            '%41' => 'A',
12685
            '%42' => 'B',
12686
            '%43' => 'C',
12687
            '%44' => 'D',
12688
            '%45' => 'E',
12689
            '%46' => 'F',
12690
            '%47' => 'G',
12691
            '%48' => 'H',
12692
            '%49' => 'I',
12693
            '%4A' => 'J',
12694
            '%4B' => 'K',
12695
            '%4C' => 'L',
12696
            '%4D' => 'M',
12697
            '%4E' => 'N',
12698
            '%4F' => 'O',
12699
            '%50' => 'P',
12700
            '%51' => 'Q',
12701
            '%52' => 'R',
12702
            '%53' => 'S',
12703
            '%54' => 'T',
12704
            '%55' => 'U',
12705
            '%56' => 'V',
12706
            '%57' => 'W',
12707
            '%58' => 'X',
12708
            '%59' => 'Y',
12709
            '%5A' => 'Z',
12710
            '%5B' => '[',
12711
            '%5C' => '\\',
12712
            '%5D' => ']',
12713
            '%5E' => '^',
12714
            '%5F' => '_',
12715
            '%60' => '`',
12716
            '%61' => 'a',
12717
            '%62' => 'b',
12718
            '%63' => 'c',
12719
            '%64' => 'd',
12720
            '%65' => 'e',
12721
            '%66' => 'f',
12722
            '%67' => 'g',
12723
            '%68' => 'h',
12724
            '%69' => 'i',
12725
            '%6A' => 'j',
12726
            '%6B' => 'k',
12727
            '%6C' => 'l',
12728
            '%6D' => 'm',
12729
            '%6E' => 'n',
12730
            '%6F' => 'o',
12731
            '%70' => 'p',
12732
            '%71' => 'q',
12733
            '%72' => 'r',
12734
            '%73' => 's',
12735
            '%74' => 't',
12736
            '%75' => 'u',
12737
            '%76' => 'v',
12738
            '%77' => 'w',
12739
            '%78' => 'x',
12740
            '%79' => 'y',
12741
            '%7A' => 'z',
12742
            '%7B' => '{',
12743
            '%7C' => '|',
12744
            '%7D' => '}',
12745
            '%7E' => '~',
12746
            '%7F' => '',
12747
            '%80' => '`',
12748
            '%81' => '',
12749
            '%82' => '‚',
12750
            '%83' => 'ƒ',
12751
            '%84' => '„',
12752
            '%85' => '…',
12753
            '%86' => '†',
12754
            '%87' => '‡',
12755
            '%88' => 'ˆ',
12756
            '%89' => '‰',
12757
            '%8A' => 'Š',
12758
            '%8B' => '‹',
12759
            '%8C' => 'Œ',
12760
            '%8D' => '',
12761
            '%8E' => 'Ž',
12762
            '%8F' => '',
12763
            '%90' => '',
12764
            '%91' => '‘',
12765
            '%92' => '’',
12766
            '%93' => '“',
12767
            '%94' => '”',
12768
            '%95' => '•',
12769
            '%96' => '–',
12770
            '%97' => '—',
12771
            '%98' => '˜',
12772
            '%99' => '™',
12773
            '%9A' => 'š',
12774
            '%9B' => '›',
12775
            '%9C' => 'œ',
12776
            '%9D' => '',
12777
            '%9E' => 'ž',
12778
            '%9F' => 'Ÿ',
12779
            '%A0' => '',
12780
            '%A1' => '¡',
12781
            '%A2' => '¢',
12782
            '%A3' => '£',
12783
            '%A4' => '¤',
12784
            '%A5' => '¥',
12785
            '%A6' => '¦',
12786
            '%A7' => '§',
12787
            '%A8' => '¨',
12788
            '%A9' => '©',
12789
            '%AA' => 'ª',
12790
            '%AB' => '«',
12791
            '%AC' => '¬',
12792
            '%AD' => '',
12793
            '%AE' => '®',
12794
            '%AF' => '¯',
12795
            '%B0' => '°',
12796
            '%B1' => '±',
12797
            '%B2' => '²',
12798
            '%B3' => '³',
12799
            '%B4' => '´',
12800
            '%B5' => 'µ',
12801
            '%B6' => '¶',
12802
            '%B7' => '·',
12803
            '%B8' => '¸',
12804
            '%B9' => '¹',
12805
            '%BA' => 'º',
12806
            '%BB' => '»',
12807
            '%BC' => '¼',
12808
            '%BD' => '½',
12809
            '%BE' => '¾',
12810
            '%BF' => '¿',
12811
            '%C0' => 'À',
12812
            '%C1' => 'Á',
12813
            '%C2' => 'Â',
12814
            '%C3' => 'Ã',
12815
            '%C4' => 'Ä',
12816
            '%C5' => 'Å',
12817
            '%C6' => 'Æ',
12818
            '%C7' => 'Ç',
12819
            '%C8' => 'È',
12820
            '%C9' => 'É',
12821
            '%CA' => 'Ê',
12822
            '%CB' => 'Ë',
12823
            '%CC' => 'Ì',
12824
            '%CD' => 'Í',
12825
            '%CE' => 'Î',
12826
            '%CF' => 'Ï',
12827
            '%D0' => 'Ð',
12828
            '%D1' => 'Ñ',
12829
            '%D2' => 'Ò',
12830
            '%D3' => 'Ó',
12831
            '%D4' => 'Ô',
12832
            '%D5' => 'Õ',
12833
            '%D6' => 'Ö',
12834
            '%D7' => '×',
12835
            '%D8' => 'Ø',
12836
            '%D9' => 'Ù',
12837
            '%DA' => 'Ú',
12838
            '%DB' => 'Û',
12839
            '%DC' => 'Ü',
12840
            '%DD' => 'Ý',
12841
            '%DE' => 'Þ',
12842
            '%DF' => 'ß',
12843
            '%E0' => 'à',
12844
            '%E1' => 'á',
12845
            '%E2' => 'â',
12846
            '%E3' => 'ã',
12847
            '%E4' => 'ä',
12848
            '%E5' => 'å',
12849
            '%E6' => 'æ',
12850
            '%E7' => 'ç',
12851
            '%E8' => 'è',
12852
            '%E9' => 'é',
12853
            '%EA' => 'ê',
12854
            '%EB' => 'ë',
12855
            '%EC' => 'ì',
12856
            '%ED' => 'í',
12857
            '%EE' => 'î',
12858
            '%EF' => 'ï',
12859
            '%F0' => 'ð',
12860
            '%F1' => 'ñ',
12861
            '%F2' => 'ò',
12862
            '%F3' => 'ó',
12863
            '%F4' => 'ô',
12864
            '%F5' => 'õ',
12865
            '%F6' => 'ö',
12866
            '%F7' => '÷',
12867
            '%F8' => 'ø',
12868
            '%F9' => 'ù',
12869
            '%FA' => 'ú',
12870
            '%FB' => 'û',
12871
            '%FC' => 'ü',
12872
            '%FD' => 'ý',
12873
            '%FE' => 'þ',
12874
            '%FF' => 'ÿ',
12875
        ];
12876
    }
12877
12878
    /**
12879
     * Decodes a UTF-8 string to ISO-8859-1.
12880
     *
12881
     * @param string $str             <p>The input string.</p>
12882
     * @param bool   $keep_utf8_chars
12883
     *
12884
     * @psalm-pure
12885
     *
12886
     * @return string
12887
     */
12888 14
    public static function utf8_decode(string $str, bool $keep_utf8_chars = false): string
12889
    {
12890 14
        if ($str === '') {
12891 6
            return '';
12892
        }
12893
12894
        // save for later comparision
12895 14
        $str_backup = $str;
12896 14
        $len = \strlen($str);
12897
12898 14
        if (self::$ORD === null) {
12899
            self::$ORD = self::getData('ord');
12900
        }
12901
12902 14
        if (self::$CHR === null) {
12903
            self::$CHR = self::getData('chr');
12904
        }
12905
12906 14
        $no_char_found = '?';
12907
        /** @noinspection ForeachInvariantsInspection */
12908 14
        for ($i = 0, $j = 0; $i < $len; ++$i, ++$j) {
12909 14
            switch ($str[$i] & "\xF0") {
12910 14
                case "\xC0":
12911 13
                case "\xD0":
12912 13
                    $c = (self::$ORD[$str[$i] & "\x1F"] << 6) | self::$ORD[$str[++$i] & "\x3F"];
12913 13
                    $str[$j] = $c < 256 ? self::$CHR[$c] : $no_char_found;
12914
12915 13
                    break;
12916
12917
                /** @noinspection PhpMissingBreakStatementInspection */
12918 13
                case "\xF0":
12919
                    ++$i;
12920
12921
                // no break
12922
12923 13
                case "\xE0":
12924 11
                    $str[$j] = $no_char_found;
12925 11
                    $i += 2;
12926
12927 11
                    break;
12928
12929
                default:
12930 12
                    $str[$j] = $str[$i];
12931
            }
12932
        }
12933
12934
        /** @var false|string $return - needed for PhpStan (stubs error) */
12935 14
        $return = \substr($str, 0, $j);
12936 14
        if ($return === false) {
12937
            $return = '';
12938
        }
12939
12940
        if (
12941 14
            $keep_utf8_chars === true
12942
            &&
12943 14
            (int) self::strlen($return) >= (int) self::strlen($str_backup)
12944
        ) {
12945 2
            return $str_backup;
12946
        }
12947
12948 14
        return $return;
12949
    }
12950
12951
    /**
12952
     * Encodes an ISO-8859-1 string to UTF-8.
12953
     *
12954
     * @param string $str <p>The input string.</p>
12955
     *
12956
     * @psalm-pure
12957
     *
12958
     * @return string
12959
     */
12960 14
    public static function utf8_encode(string $str): string
12961
    {
12962 14
        if ($str === '') {
12963 14
            return '';
12964
        }
12965
12966
        /** @var false|string $str - the polyfill maybe return false */
12967 14
        $str = \utf8_encode($str);
0 ignored issues
show
Bug introduced by
It seems like $str can also be of type false; however, parameter $data of utf8_encode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

12967
        $str = \utf8_encode(/** @scrutinizer ignore-type */ $str);
Loading history...
12968
12969
        /** @noinspection CallableParameterUseCaseInTypeContextInspection */
12970
        /** @psalm-suppress TypeDoesNotContainType */
12971 14
        if ($str === false) {
12972
            return '';
12973
        }
12974
12975 14
        return $str;
12976
    }
12977
12978
    /**
12979
     * fix -> utf8-win1252 chars
12980
     *
12981
     * @param string $str <p>The input string.</p>
12982
     *
12983
     * @psalm-pure
12984
     *
12985
     * @return string
12986
     *
12987
     * @deprecated <p>please use "UTF8::fix_simple_utf8()"</p>
12988
     */
12989 2
    public static function utf8_fix_win1252_chars(string $str): string
12990
    {
12991 2
        return self::fix_simple_utf8($str);
12992
    }
12993
12994
    /**
12995
     * Returns an array with all utf8 whitespace characters.
12996
     *
12997
     * @see http://www.bogofilter.org/pipermail/bogofilter/2003-March/001889.html
12998
     *
12999
     * @psalm-pure
13000
     *
13001
     * @return string[]
13002
     *                  An array with all known whitespace characters as values and the type of whitespace as keys
13003
     *                  as defined in above URL
13004
     */
13005 2
    public static function whitespace_table(): array
13006
    {
13007 2
        return self::$WHITESPACE_TABLE;
13008
    }
13009
13010
    /**
13011
     * Limit the number of words in a string.
13012
     *
13013
     * @param string $str        <p>The input string.</p>
13014
     * @param int    $limit      <p>The limit of words as integer.</p>
13015
     * @param string $str_add_on <p>Replacement for the striped string.</p>
13016
     *
13017
     * @psalm-pure
13018
     *
13019
     * @return string
13020
     */
13021 2
    public static function words_limit(
13022
        string $str,
13023
        int $limit = 100,
13024
        string $str_add_on = '…'
13025
    ): string {
13026 2
        if ($str === '' || $limit < 1) {
13027 2
            return '';
13028
        }
13029
13030 2
        \preg_match('/^\\s*+(?:[^\\s]++\\s*+){1,' . $limit . '}/u', $str, $matches);
13031
13032
        if (
13033 2
            !isset($matches[0])
13034
            ||
13035 2
            \mb_strlen($str) === (int) \mb_strlen($matches[0])
13036
        ) {
13037 2
            return $str;
13038
        }
13039
13040 2
        return \rtrim($matches[0]) . $str_add_on;
13041
    }
13042
13043
    /**
13044
     * Wraps a string to a given number of characters
13045
     *
13046
     * @see http://php.net/manual/en/function.wordwrap.php
13047
     *
13048
     * @param string $str   <p>The input string.</p>
13049
     * @param int    $width [optional] <p>The column width.</p>
13050
     * @param string $break [optional] <p>The line is broken using the optional break parameter.</p>
13051
     * @param bool   $cut   [optional] <p>
13052
     *                      If the cut is set to true, the string is
13053
     *                      always wrapped at or before the specified width. So if you have
13054
     *                      a word that is larger than the given width, it is broken apart.
13055
     *                      </p>
13056
     *
13057
     * @psalm-pure
13058
     *
13059
     * @return string
13060
     *                <p>The given string wrapped at the specified column.</p>
13061
     */
13062 12
    public static function wordwrap(
13063
        string $str,
13064
        int $width = 75,
13065
        string $break = "\n",
13066
        bool $cut = false
13067
    ): string {
13068 12
        if ($str === '' || $break === '') {
13069 4
            return '';
13070
        }
13071
13072 10
        $str_split = \explode($break, $str);
13073 10
        if ($str_split === false) {
13074
            return '';
13075
        }
13076
13077 10
        $chars = [];
13078 10
        $word_split = '';
13079 10
        foreach ($str_split as $i => $i_value) {
13080 10
            if ($i) {
13081 3
                $chars[] = $break;
13082 3
                $word_split .= '#';
13083
            }
13084
13085 10
            foreach (self::str_split($i_value) as $c) {
13086 10
                $chars[] = $c;
13087 10
                if ($c === ' ') {
13088 3
                    $word_split .= ' ';
13089
                } else {
13090 10
                    $word_split .= '?';
13091
                }
13092
            }
13093
        }
13094
13095 10
        $str_return = '';
13096 10
        $j = 0;
13097 10
        $b = -1;
13098 10
        $i = -1;
13099 10
        $word_split = \wordwrap($word_split, $width, '#', $cut);
13100
13101 10
        $max = \mb_strlen($word_split);
13102 10
        while (($b = \mb_strpos($word_split, '#', $b + 1)) !== false) {
13103 8
            for (++$i; $i < $b; ++$i) {
13104 8
                $str_return .= $chars[$j];
13105 8
                unset($chars[$j++]);
13106
13107
                // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
13108 8
                if ($i > $max) {
13109
                    break 2;
13110
                }
13111
            }
13112
13113
            if (
13114 8
                $break === $chars[$j]
13115
                ||
13116 8
                $chars[$j] === ' '
13117
            ) {
13118 5
                unset($chars[$j++]);
13119
            }
13120
13121 8
            $str_return .= $break;
13122
13123
            // prevent endless loop, e.g. if there is a error in the "mb_*" polyfill
13124 8
            if ($b > $max) {
13125
                break;
13126
            }
13127
        }
13128
13129 10
        return $str_return . \implode('', $chars);
13130
    }
13131
13132
    /**
13133
     * Line-Wrap the string after $limit, but split the string by "$delimiter" before ...
13134
     *    ... so that we wrap the per line.
13135
     *
13136
     * @param string      $str             <p>The input string.</p>
13137
     * @param int         $width           [optional] <p>The column width.</p>
13138
     * @param string      $break           [optional] <p>The line is broken using the optional break parameter.</p>
13139
     * @param bool        $cut             [optional] <p>
13140
     *                                     If the cut is set to true, the string is
13141
     *                                     always wrapped at or before the specified width. So if you have
13142
     *                                     a word that is larger than the given width, it is broken apart.
13143
     *                                     </p>
13144
     * @param bool        $add_final_break [optional] <p>
13145
     *                                     If this flag is true, then the method will add a $break at the end
13146
     *                                     of the result string.
13147
     *                                     </p>
13148
     * @param string|null $delimiter       [optional] <p>
13149
     *                                     You can change the default behavior, where we split the string by newline.
13150
     *                                     </p>
13151
     *
13152
     * @psalm-pure
13153
     *
13154
     * @return string
13155
     */
13156 1
    public static function wordwrap_per_line(
13157
        string $str,
13158
        int $width = 75,
13159
        string $break = "\n",
13160
        bool $cut = false,
13161
        bool $add_final_break = true,
13162
        string $delimiter = null
13163
    ): string {
13164 1
        if ($delimiter === null) {
13165 1
            $strings = \preg_split('/\\r\\n|\\r|\\n/', $str);
13166
        } else {
13167 1
            $strings = \explode($delimiter, $str);
13168
        }
13169
13170 1
        $string_helper_array = [];
13171 1
        if ($strings !== false) {
13172 1
            foreach ($strings as $value) {
13173 1
                $string_helper_array[] = self::wordwrap($value, $width, $break, $cut);
13174
            }
13175
        }
13176
13177 1
        if ($add_final_break) {
13178 1
            $final_break = $break;
13179
        } else {
13180 1
            $final_break = '';
13181
        }
13182
13183 1
        return \implode($delimiter ?? "\n", $string_helper_array) . $final_break;
13184
    }
13185
13186
    /**
13187
     * Returns an array of Unicode White Space characters.
13188
     *
13189
     * @psalm-pure
13190
     *
13191
     * @return string[] an array with numeric code point as key and White Space Character as value
13192
     */
13193 2
    public static function ws(): array
13194
    {
13195 2
        return self::$WHITESPACE;
13196
    }
13197
13198
    /**
13199
     * Checks whether the passed string contains only byte sequences that are valid UTF-8 characters.
13200
     *
13201
     * @see          http://hsivonen.iki.fi/php-utf8/
13202
     *
13203
     * @param string $str    <p>The string to be checked.</p>
13204
     * @param bool   $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
13205
     *
13206
     * @psalm-pure
13207
     *
13208
     * @return bool
13209
     *
13210
     * @noinspection ReturnTypeCanBeDeclaredInspection
13211
     */
13212 108
    private static function is_utf8_string(string $str, bool $strict = false)
13213
    {
13214 108
        if ($str === '') {
13215 14
            return true;
13216
        }
13217
13218 102
        if ($strict === true) {
13219 2
            $is_binary = self::is_binary($str, true);
13220
13221 2
            if ($is_binary && self::is_utf16($str, false) !== false) {
13222 2
                return false;
13223
            }
13224
13225
            if ($is_binary && self::is_utf32($str, false) !== false) {
13226
                return false;
13227
            }
13228
        }
13229
13230 102
        if (self::pcre_utf8_support() !== true) {
13231
            // If even just the first character can be matched, when the /u
13232
            // modifier is used, then it's valid UTF-8. If the UTF-8 is somehow
13233
            // invalid, nothing at all will match, even if the string contains
13234
            // some valid sequences
13235
            return \preg_match('/^./us', $str, $ar) === 1;
13236
        }
13237
13238 102
        $mState = 0; // cached expected number of octets after the current octet
13239
        // until the beginning of the next UTF8 character sequence
13240 102
        $mUcs4 = 0; // cached Unicode character
13241 102
        $mBytes = 1; // cached expected number of octets in the current sequence
13242
13243 102
        if (self::$ORD === null) {
13244
            self::$ORD = self::getData('ord');
13245
        }
13246
13247 102
        $len = \strlen($str);
13248
        /** @noinspection ForeachInvariantsInspection */
13249 102
        for ($i = 0; $i < $len; ++$i) {
13250 102
            $in = self::$ORD[$str[$i]];
13251
13252 102
            if ($mState === 0) {
13253
                // When mState is zero we expect either a US-ASCII character or a
13254
                // multi-octet sequence.
13255 102
                if ((0x80 & $in) === 0) {
13256
                    // US-ASCII, pass straight through.
13257 97
                    $mBytes = 1;
13258 83
                } elseif ((0xE0 & $in) === 0xC0) {
13259
                    // First octet of 2 octet sequence.
13260 73
                    $mUcs4 = $in;
13261 73
                    $mUcs4 = ($mUcs4 & 0x1F) << 6;
13262 73
                    $mState = 1;
13263 73
                    $mBytes = 2;
13264 58
                } elseif ((0xF0 & $in) === 0xE0) {
13265
                    // First octet of 3 octet sequence.
13266 42
                    $mUcs4 = $in;
13267 42
                    $mUcs4 = ($mUcs4 & 0x0F) << 12;
13268 42
                    $mState = 2;
13269 42
                    $mBytes = 3;
13270 29
                } elseif ((0xF8 & $in) === 0xF0) {
13271
                    // First octet of 4 octet sequence.
13272 18
                    $mUcs4 = $in;
13273 18
                    $mUcs4 = ($mUcs4 & 0x07) << 18;
13274 18
                    $mState = 3;
13275 18
                    $mBytes = 4;
13276 13
                } elseif ((0xFC & $in) === 0xF8) {
13277
                    /* First octet of 5 octet sequence.
13278
                     *
13279
                     * This is illegal because the encoded codepoint must be either
13280
                     * (a) not the shortest form or
13281
                     * (b) outside the Unicode range of 0-0x10FFFF.
13282
                     * Rather than trying to resynchronize, we will carry on until the end
13283
                     * of the sequence and let the later error handling code catch it.
13284
                     */
13285 5
                    $mUcs4 = $in;
13286 5
                    $mUcs4 = ($mUcs4 & 0x03) << 24;
13287 5
                    $mState = 4;
13288 5
                    $mBytes = 5;
13289 10
                } elseif ((0xFE & $in) === 0xFC) {
13290
                    // First octet of 6 octet sequence, see comments for 5 octet sequence.
13291 5
                    $mUcs4 = $in;
13292 5
                    $mUcs4 = ($mUcs4 & 1) << 30;
13293 5
                    $mState = 5;
13294 5
                    $mBytes = 6;
13295
                } else {
13296
                    // Current octet is neither in the US-ASCII range nor a legal first
13297
                    // octet of a multi-octet sequence.
13298 102
                    return false;
13299
                }
13300 83
            } elseif ((0xC0 & $in) === 0x80) {
13301
13302
                // When mState is non-zero, we expect a continuation of the multi-octet
13303
                // sequence
13304
13305
                // Legal continuation.
13306 75
                $shift = ($mState - 1) * 6;
13307 75
                $tmp = $in;
13308 75
                $tmp = ($tmp & 0x0000003F) << $shift;
13309 75
                $mUcs4 |= $tmp;
13310
                // Prefix: End of the multi-octet sequence. mUcs4 now contains the final
13311
                // Unicode code point to be output.
13312 75
                if (--$mState === 0) {
13313
                    // Check for illegal sequences and code points.
13314
                    //
13315
                    // From Unicode 3.1, non-shortest form is illegal
13316
                    if (
13317 75
                        ($mBytes === 2 && $mUcs4 < 0x0080)
13318
                        ||
13319 75
                        ($mBytes === 3 && $mUcs4 < 0x0800)
13320
                        ||
13321 75
                        ($mBytes === 4 && $mUcs4 < 0x10000)
13322
                        ||
13323 75
                        ($mBytes > 4)
13324
                        ||
13325
                        // From Unicode 3.2, surrogate characters are illegal.
13326 75
                        (($mUcs4 & 0xFFFFF800) === 0xD800)
13327
                        ||
13328
                        // Code points outside the Unicode range are illegal.
13329 75
                        ($mUcs4 > 0x10FFFF)
13330
                    ) {
13331 9
                        return false;
13332
                    }
13333
                    // initialize UTF8 cache
13334 75
                    $mState = 0;
13335 75
                    $mUcs4 = 0;
13336 75
                    $mBytes = 1;
13337
                }
13338
            } else {
13339
                // ((0xC0 & (*in) != 0x80) && (mState != 0))
13340
                // Incomplete multi-octet sequence.
13341 35
                return false;
13342
            }
13343
        }
13344
13345 67
        return true;
13346
    }
13347
13348
    /**
13349
     * @param string $str
13350
     * @param bool   $use_lowercase      <p>Use uppercase by default, otherwise use lowercase.</p>
13351
     * @param bool   $use_full_case_fold <p>Convert not only common cases.</p>
13352
     *
13353
     * @psalm-pure
13354
     *
13355
     * @return string
13356
     *
13357
     * @noinspection ReturnTypeCanBeDeclaredInspection
13358
     */
13359 33
    private static function fixStrCaseHelper(
13360
        string $str,
13361
        $use_lowercase = false,
13362
        $use_full_case_fold = false
13363
    ) {
13364 33
        $upper = self::$COMMON_CASE_FOLD['upper'];
13365 33
        $lower = self::$COMMON_CASE_FOLD['lower'];
13366
13367 33
        if ($use_lowercase === true) {
13368 2
            $str = \str_replace(
13369 2
                $upper,
13370 2
                $lower,
13371 2
                $str
13372
            );
13373
        } else {
13374 31
            $str = \str_replace(
13375 31
                $lower,
13376 31
                $upper,
13377 31
                $str
13378
            );
13379
        }
13380
13381 33
        if ($use_full_case_fold) {
13382
            /**
13383
             * @psalm-suppress ImpureStaticVariable
13384
             *
13385
             * @var array<mixed>|null
13386
             */
13387 31
            static $FULL_CASE_FOLD = null;
13388 31
            if ($FULL_CASE_FOLD === null) {
13389 1
                $FULL_CASE_FOLD = self::getData('caseFolding_full');
13390
            }
13391
13392 31
            if ($use_lowercase === true) {
13393 2
                $str = \str_replace($FULL_CASE_FOLD[0], $FULL_CASE_FOLD[1], $str);
13394
            } else {
13395 29
                $str = \str_replace($FULL_CASE_FOLD[1], $FULL_CASE_FOLD[0], $str);
13396
            }
13397
        }
13398
13399 33
        return $str;
13400
    }
13401
13402
    /**
13403
     * get data from "/data/*.php"
13404
     *
13405
     * @param string $file
13406
     *
13407
     * @psalm-pure
13408
     *
13409
     * @return array
13410
     *
13411
     * @noinspection ReturnTypeCanBeDeclaredInspection
13412
     */
13413 6
    private static function getData(string $file)
13414
    {
13415
        /** @noinspection PhpIncludeInspection */
13416
        /** @noinspection UsingInclusionReturnValueInspection */
13417
        /** @psalm-suppress UnresolvableInclude */
13418 6
        return include __DIR__ . '/data/' . $file . '.php';
13419
    }
13420
13421
    /**
13422
     * @psalm-pure
13423
     *
13424
     * @return true|null
13425
     */
13426 12
    private static function initEmojiData()
13427
    {
13428 12
        if (self::$EMOJI_KEYS_CACHE === null) {
13429 1
            if (self::$EMOJI === null) {
13430 1
                self::$EMOJI = self::getData('emoji');
13431
            }
13432
13433 1
            \uksort(
13434 1
                self::$EMOJI,
13435
                static function (string $a, string $b): int {
13436 1
                    return \strlen($b) <=> \strlen($a);
13437 1
                }
13438
            );
13439
13440 1
            self::$EMOJI_KEYS_CACHE = \array_keys(self::$EMOJI);
13441 1
            self::$EMOJI_VALUES_CACHE = self::$EMOJI;
13442
13443 1
            foreach (self::$EMOJI_KEYS_CACHE as $key) {
13444 1
                $tmp_key = \crc32($key);
13445 1
                self::$EMOJI_KEYS_REVERSIBLE_CACHE[] = '_-_PORTABLE_UTF8_-_' . $tmp_key . '_-_' . \strrev((string) $tmp_key) . '_-_8FTU_ELBATROP_-_';
13446
            }
13447
13448 1
            return true;
13449
        }
13450
13451 12
        return null;
13452
    }
13453
13454
    /**
13455
     * Checks whether mbstring "overloaded" is active on the server.
13456
     *
13457
     * @psalm-pure
13458
     *
13459
     * @return bool
13460
     *
13461
     * @noinspection ReturnTypeCanBeDeclaredInspection
13462
     */
13463
    private static function mbstring_overloaded()
13464
    {
13465
        /**
13466
         * INI directive 'mbstring.func_overload' is deprecated since PHP 7.2
13467
         */
13468
13469
        /** @noinspection PhpComposerExtensionStubsInspection */
13470
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
13471
        return \defined('MB_OVERLOAD_STRING')
13472
               &&
13473
               ((int) @\ini_get('mbstring.func_overload') & \MB_OVERLOAD_STRING);
13474
    }
13475
13476
    /**
13477
     * @param array    $strings
13478
     * @param bool     $remove_empty_values
13479
     * @param int|null $remove_short_values
13480
     *
13481
     * @psalm-pure
13482
     *
13483
     * @return array
13484
     *
13485
     * @noinspection ReturnTypeCanBeDeclaredInspection
13486
     */
13487 2
    private static function reduce_string_array(
13488
        array $strings,
13489
        bool $remove_empty_values,
13490
        int $remove_short_values = null
13491
    ) {
13492
        // init
13493 2
        $return = [];
13494
13495 2
        foreach ($strings as &$str) {
13496
            if (
13497 2
                $remove_short_values !== null
13498
                &&
13499 2
                \mb_strlen($str) <= $remove_short_values
13500
            ) {
13501 2
                continue;
13502
            }
13503
13504
            if (
13505 2
                $remove_empty_values === true
13506
                &&
13507 2
                \trim($str) === ''
13508
            ) {
13509 2
                continue;
13510
            }
13511
13512 2
            $return[] = $str;
13513
        }
13514
13515 2
        return $return;
13516
    }
13517
13518
    /**
13519
     * rxClass
13520
     *
13521
     * @param string $s
13522
     * @param string $class
13523
     *
13524
     * @psalm-pure
13525
     *
13526
     * @return string
13527
     *
13528
     * @noinspection ReturnTypeCanBeDeclaredInspection
13529
     */
13530 33
    private static function rxClass(string $s, string $class = '')
13531
    {
13532
        /**
13533
         * @psalm-suppress ImpureStaticVariable
13534
         *
13535
         * @var array<string,string>
13536
         */
13537 33
        static $RX_CLASS_CACHE = [];
13538
13539 33
        $cache_key = $s . '_' . $class;
13540
13541 33
        if (isset($RX_CLASS_CACHE[$cache_key])) {
13542 21
            return $RX_CLASS_CACHE[$cache_key];
13543
        }
13544
13545 16
        $class_array = [$class];
13546
13547
        /** @noinspection SuspiciousLoopInspection */
13548
        /** @noinspection AlterInForeachInspection */
13549 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...
13550 15
            if ($s === '-') {
13551
                $class_array[0] = '-' . $class_array[0];
13552 15
            } elseif (!isset($s[2])) {
13553 15
                $class_array[0] .= \preg_quote($s, '/');
13554 1
            } elseif (self::strlen($s) === 1) {
13555 1
                $class_array[0] .= $s;
13556
            } else {
13557 15
                $class_array[] = $s;
13558
            }
13559
        }
13560
13561 16
        if ($class_array[0]) {
13562 16
            $class_array[0] = '[' . $class_array[0] . ']';
13563
        }
13564
13565 16
        if (\count($class_array) === 1) {
13566 16
            $return = $class_array[0];
13567
        } else {
13568
            $return = '(?:' . \implode('|', $class_array) . ')';
13569
        }
13570
13571 16
        $RX_CLASS_CACHE[$cache_key] = $return;
13572
13573 16
        return $return;
13574
    }
13575
13576
    /**
13577
     * Personal names such as "Marcus Aurelius" are sometimes typed incorrectly using lowercase ("marcus aurelius").
13578
     *
13579
     * @param string $names
13580
     * @param string $delimiter
13581
     * @param string $encoding
13582
     *
13583
     * @psalm-pure
13584
     *
13585
     * @return string
13586
     *
13587
     * @noinspection ReturnTypeCanBeDeclaredInspection
13588
     */
13589 1
    private static function str_capitalize_name_helper(
13590
        string $names,
13591
        string $delimiter,
13592
        string $encoding = 'UTF-8'
13593
    ) {
13594
        // init
13595 1
        $name_helper_array = \explode($delimiter, $names);
13596 1
        if ($name_helper_array === false) {
13597
            return '';
13598
        }
13599
13600
        $special_cases = [
13601 1
            'names' => [
13602
                'ab',
13603
                'af',
13604
                'al',
13605
                'and',
13606
                'ap',
13607
                'bint',
13608
                'binte',
13609
                'da',
13610
                'de',
13611
                'del',
13612
                'den',
13613
                'der',
13614
                'di',
13615
                'dit',
13616
                'ibn',
13617
                'la',
13618
                'mac',
13619
                'nic',
13620
                'of',
13621
                'ter',
13622
                'the',
13623
                'und',
13624
                'van',
13625
                'von',
13626
                'y',
13627
                'zu',
13628
            ],
13629
            'prefixes' => [
13630
                'al-',
13631
                "d'",
13632
                'ff',
13633
                "l'",
13634
                'mac',
13635
                'mc',
13636
                'nic',
13637
            ],
13638
        ];
13639
13640 1
        foreach ($name_helper_array as &$name) {
13641 1
            if (\in_array($name, $special_cases['names'], true)) {
13642 1
                continue;
13643
            }
13644
13645 1
            $continue = false;
13646
13647 1
            if ($delimiter === '-') {
13648
                /** @noinspection AlterInForeachInspection */
13649 1
                foreach ((array) $special_cases['names'] as &$beginning) {
13650 1
                    if (self::strpos($name, $beginning, 0, $encoding) === 0) {
13651 1
                        $continue = true;
13652
                    }
13653
                }
13654
            }
13655
13656
            /** @noinspection AlterInForeachInspection */
13657 1
            foreach ((array) $special_cases['prefixes'] as &$beginning) {
13658 1
                if (self::strpos($name, $beginning, 0, $encoding) === 0) {
13659 1
                    $continue = true;
13660
                }
13661
            }
13662
13663 1
            if ($continue === true) {
13664 1
                continue;
13665
            }
13666
13667 1
            $name = self::ucfirst($name);
13668
        }
13669
13670 1
        return \implode($delimiter, $name_helper_array);
13671
    }
13672
13673
    /**
13674
     * Generic case-sensitive transformation for collation matching.
13675
     *
13676
     * @param string $str <p>The input string</p>
13677
     *
13678
     * @psalm-pure
13679
     *
13680
     * @return string|null
13681
     */
13682 6
    private static function strtonatfold(string $str)
13683
    {
13684
        /** @noinspection PhpUndefinedClassInspection */
13685 6
        return \preg_replace(
13686 6
            '/\p{Mn}+/u',
13687 6
            '',
13688 6
            \Normalizer::normalize($str, \Normalizer::NFD)
13689
        );
13690
    }
13691
13692
    /**
13693
     * @param int|string $input
13694
     *
13695
     * @psalm-pure
13696
     *
13697
     * @return string
13698
     *
13699
     * @noinspection ReturnTypeCanBeDeclaredInspection
13700
     */
13701 31
    private static function to_utf8_convert_helper($input)
13702
    {
13703
        // init
13704 31
        $buf = '';
13705
13706 31
        if (self::$ORD === null) {
13707 1
            self::$ORD = self::getData('ord');
13708
        }
13709
13710 31
        if (self::$CHR === null) {
13711 1
            self::$CHR = self::getData('chr');
13712
        }
13713
13714 31
        if (self::$WIN1252_TO_UTF8 === null) {
13715 1
            self::$WIN1252_TO_UTF8 = self::getData('win1252_to_utf8');
13716
        }
13717
13718 31
        $ordC1 = self::$ORD[$input];
13719 31
        if (isset(self::$WIN1252_TO_UTF8[$ordC1])) { // found in Windows-1252 special cases
13720 31
            $buf .= self::$WIN1252_TO_UTF8[$ordC1];
13721
        } else {
13722
            /** @noinspection OffsetOperationsInspection */
13723 1
            $cc1 = self::$CHR[$ordC1 / 64] | "\xC0";
13724 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...
13725 1
            $buf .= $cc1 . $cc2;
13726
        }
13727
13728 31
        return $buf;
13729
    }
13730
13731
    /**
13732
     * @param string $str
13733
     *
13734
     * @psalm-pure
13735
     *
13736
     * @return string
13737
     *
13738
     * @noinspection ReturnTypeCanBeDeclaredInspection
13739
     */
13740 10
    private static function urldecode_unicode_helper(string $str)
13741
    {
13742 10
        $pattern = '/%u([0-9a-fA-F]{3,4})/';
13743 10
        if (\preg_match($pattern, $str)) {
13744 7
            $str = (string) \preg_replace($pattern, '&#x\\1;', $str);
13745
        }
13746
13747 10
        return $str;
13748
    }
13749
}
13750