Passed
Push — master ( 2357d3...eb0a13 )
by Lars
15:49 queued 13:24
created

UTF8   F

Complexity

Total Complexity 1738

Size/Duplication

Total Lines 14073
Duplicated Lines 0 %

Test Coverage

Coverage 80.77%

Importance

Changes 104
Bugs 54 Features 6
Metric Value
eloc 4447
c 104
b 54
f 6
dl 0
loc 14073
ccs 2928
cts 3625
cp 0.8077
rs 0.8
wmc 1738

308 Methods

Rating   Name   Duplication   Size   Complexity  
A binary_to_str() 0 12 3
B chr_to_decimal() 0 38 8
A add_bom_to_string() 0 7 2
A array_change_key_case() 0 23 5
D chr() 0 113 19
A chr_to_int() 0 3 1
A chunk_split() 0 3 1
A clean() 0 48 6
A __construct() 0 2 1
A codepoints() 0 36 5
A chr_map() 0 5 1
A cleanup() 0 25 2
A char_at() 0 7 2
A chars() 0 4 1
A chr_size_list() 0 17 3
A checkForSupport() 0 47 4
A chr_to_hex() 0 11 3
A collapse_whitespace() 0 8 2
A access() 0 11 4
A callback() 0 3 1
A bom() 0 3 1
A str_substr_after_first_separator() 0 28 6
A file_has_bom() 0 8 2
A str_begins() 0 3 1
A max() 0 14 3
B str_camelize() 0 74 10
A parse_str() 0 18 4
A filter_input() 0 16 3
A str_contains() 0 10 2
B str_to_lines() 0 29 8
A substr_in_byte() 0 18 6
A stripos_in_byte() 0 12 4
A get_unique_string() 0 22 3
A is_bom() 0 10 3
A is_hexadecimal() 0 8 2
A strnatcasecmp() 0 5 1
A encode_mimeheader() 0 26 5
A substr_left() 0 15 4
A count_chars() 0 11 1
D strlen() 0 104 19
A str_isubstr_last() 0 25 4
A to_int() 0 7 2
A ctype_loaded() 0 3 1
A str_replace_beginning() 0 24 6
A has_uppercase() 0 8 2
A remove_left() 0 24 4
B stripos() 0 59 11
A str_offset_exists() 0 10 2
D strrchr() 0 104 20
A to_filename() 0 9 1
A str_iends_with() 0 11 3
A max_chr_width() 0 8 2
A isBinary() 0 3 1
C utf8_decode() 0 61 13
A ltrim() 0 27 5
A emoji_decode() 0 18 2
A is_utf8() 0 13 4
A remove_html() 0 3 1
B str_longest_common_suffix() 0 54 10
C wordwrap() 0 72 15
B ucfirst() 0 57 7
A lcword() 0 13 1
A str_pad_both() 0 12 1
A str_index_last() 0 11 1
A str_substr_last() 0 33 6
A mbstring_loaded() 0 3 1
A str_limit() 0 26 6
A html_escape() 0 6 1
A toUTF8() 0 3 1
A string() 0 12 3
D normalize_encoding() 0 147 16
B rxClass() 0 45 8
B get_file_type() 0 65 7
A str_ensure_right() 0 13 4
B str_titleize_for_humans() 0 171 7
C is_utf16() 0 71 16
A isHtml() 0 3 1
C filter() 0 51 12
A normalize_whitespace() 0 9 1
A str_starts_with() 0 11 3
A isBase64() 0 3 1
A str_humanize() 0 15 1
A is_html() 0 14 2
C substr_count_in_byte() 0 55 15
A decode_mimeheader() 0 8 3
A html_decode() 0 6 1
A strchr() 0 13 1
A strichr() 0 13 1
A isUtf32() 0 3 1
A str_index_first() 0 11 1
A strlen_in_byte() 0 12 3
A str_ireplace_ending() 0 21 6
A rtrim() 0 27 5
C str_longest_common_substring() 0 76 16
A regex_replace() 0 20 3
A titlecase() 0 35 5
A getData() 0 6 1
A str_iindex_first() 0 11 1
B strtolower() 0 60 10
B urldecode() 0 51 8
A str_isubstr_before_first_separator() 0 19 5
B strrev() 0 43 10
A replace_all() 0 11 2
D substr_replace() 0 124 27
A removeBOM() 0 3 1
A strstr_in_byte() 0 15 4
A emoji_encode() 0 18 2
A str_matches_pattern() 0 3 1
A is_alpha() 0 8 2
C str_titleize() 0 69 12
A str_split_array() 0 17 2
A ws() 0 3 1
B get_random_string() 0 56 10
A str_replace_first() 0 20 2
A fix_utf8() 0 30 4
A toLatin1() 0 3 1
A str_pad_right() 0 12 1
B ucwords() 0 51 9
A first_char() 0 14 4
A css_identifier() 0 56 6
A to_boolean() 0 35 5
C stristr() 0 71 15
A isUtf8() 0 3 1
A strncasecmp() 0 10 1
B strwidth() 0 43 8
A str_iends() 0 3 1
A css_stripe_media_queries() 0 6 1
A trim() 0 27 5
A is_serialized() 0 11 3
A str_upper_camelize() 0 8 1
A is_uppercase() 0 8 2
A substr_compare() 0 33 6
C substr_count() 0 65 16
A strnatcmp() 0 9 2
D str_pad() 0 146 16
A urldecode_unicode_helper() 0 8 2
A str_ireplace() 0 20 3
A to_latin1() 0 3 1
A str_replace_ending() 0 24 6
A string_has_bom() 0 10 3
B strtr() 0 42 11
B str_contains_all() 0 24 9
A is_ascii() 0 3 1
A normalize_line_ending() 0 3 1
A str_isubstr_after_last_separator() 0 26 5
D range() 0 72 23
B strspn() 0 30 10
A strcasecmp() 0 21 1
A str_transliterate() 0 6 1
B rawurldecode() 0 51 8
A str_ends() 0 3 1
B str_capitalize_name_helper() 0 86 10
A utf8_encode() 0 16 3
A normalize_msword() 0 3 1
C str_detect_encoding() 0 111 13
A spaces_to_tabs() 0 11 3
A str_istarts_with() 0 11 3
A is_blank() 0 8 2
A str_replace() 0 14 1
A substr_iright() 0 15 4
D getCharDirection() 0 105 118
A htmlspecialchars() 0 15 3
A replace() 0 11 2
A filter_var_array() 0 15 2
A decimal_to_chr() 0 3 1
A to_iso8859() 0 16 4
A has_whitespace() 0 8 2
A words_limit() 0 20 5
A strip_tags() 0 18 4
A pcre_utf8_support() 0 4 1
B between() 0 48 8
A str_isubstr_before_last_separator() 0 24 6
D str_truncate_safe() 0 86 18
A substr_right() 0 31 6
A lowerCaseFirst() 0 13 1
D str_split() 0 138 29
A str_ends_with_any() 0 13 4
A strrpos_in_byte() 0 12 4
A remove_right() 0 25 4
F strrpos() 0 122 25
A remove_html_breaks() 0 3 1
A showSupport() 0 17 3
A remove_invisible_characters() 0 9 1
A single_chr_html_encode() 0 18 4
A str_replace_last() 0 19 2
A str_iindex_last() 0 11 1
A str_substr_before_last_separator() 0 31 6
B is_binary() 0 38 9
A intlChar_loaded() 0 3 1
B strtocasefold() 0 33 7
A lcfirst() 0 44 5
A tabs_to_spaces() 0 11 3
B is_url() 0 44 7
A finfo_loaded() 0 3 1
B str_truncate() 0 44 7
D strripos() 0 99 19
A strpos_in_byte() 0 12 4
A str_ends_with() 0 11 3
A fits_inside() 0 3 1
A to_ascii() 0 6 1
A is_binary_file() 0 16 3
A intl_loaded() 0 3 1
A reduce_string_array() 0 29 6
B str_longest_common_prefix() 0 51 8
A mbstring_overloaded() 0 11 2
A str_pad_left() 0 12 1
A str_substr_first() 0 33 6
A html_stripe_empty_tags() 0 6 1
A remove_bom() 0 22 5
A str_repeat() 0 5 1
A strpbrk() 0 11 4
A whitespace_table() 0 3 1
A substr_count_simple() 0 31 6
A str_iends_with_any() 0 13 4
A str_substr_after_last_separator() 0 28 6
A str_isubstr_after_first_separator() 0 26 5
F extract_text() 0 175 34
A json_loaded() 0 3 1
A isBom() 0 3 1
B str_snakeize() 0 57 6
A int_to_chr() 0 3 1
A is_lowercase() 0 8 2
A str_sort() 0 15 3
D to_utf8() 0 119 35
A ucword() 0 6 1
A str_underscored() 0 3 1
A str_offset_get() 0 14 4
A strip_whitespace() 0 7 2
A hasBom() 0 3 1
A toAscii() 0 6 1
A str_ibegins() 0 3 1
A str_capitalize_name() 0 8 1
B str_limit_after_word() 0 53 11
A iconv_loaded() 0 3 1
A lcwords() 0 34 6
A str_upper_first() 0 13 1
A isAscii() 0 3 1
A normalizeEncoding() 0 3 1
A swapCase() 0 17 4
A filter_var() 0 15 2
A substr_ileft() 0 15 4
A is_empty() 0 3 1
B html_encode() 0 53 11
A str_dasherize() 0 3 1
A isUtf16() 0 3 1
A str_ensure_left() 0 11 3
F encode() 0 144 37
B urldecode_fix_win1252_chars() 0 227 1
A toIso8859() 0 3 1
C is_utf32() 0 71 16
C ord() 0 77 16
B to_string() 0 26 7
A is_alphanumeric() 0 8 2
A strtonatfold() 0 7 1
A json_decode() 0 14 2
A fix_simple_utf8() 0 32 4
C strcspn() 0 52 12
A is_printable() 0 3 1
B is_json() 0 27 8
A fixStrCaseHelper() 0 41 5
A int_to_hex() 0 7 2
B str_split_pattern() 0 49 11
D strstr() 0 95 18
A has_lowercase() 0 8 2
A json_encode() 0 10 2
A str_isubstr_first() 0 25 4
A is_base64() 0 20 5
A str_last_char() 0 16 4
A str_ireplace_beginning() 0 21 6
A hex_to_int() 0 14 3
A htmlentities() 0 28 3
A hex_to_chr() 0 4 1
A str_substr_before_first_separator() 0 32 6
F substr() 0 142 32
A isJson() 0 3 1
A wordwrap_per_line() 0 28 5
A str_surround() 0 3 1
A strncmp() 0 19 4
A filter_input_array() 0 15 3
A str_insert() 0 28 4
A getSupportInfo() 0 13 3
A utf8_fix_win1252_chars() 0 3 1
A replace_diamond_question_mark() 0 42 5
D is_utf8_string() 0 134 28
A to_utf8_convert_helper() 0 28 5
B str_delimit() 0 33 8
B strtoupper() 0 60 10
A min() 0 14 3
A is_punctuation() 0 3 1
C html_entity_decode() 0 58 13
A str_starts_with_any() 0 17 5
B strrichr() 0 54 11
A split() 0 7 1
A str_istarts_with_any() 0 17 5
B str_contains_any() 0 29 8
A initEmojiData() 0 29 4
A remove_duplicates() 0 16 4
B str_slice() 0 33 10
F strpos() 0 137 27
A str_shuffle() 0 35 6
A strcmp() 0 9 2
B file_get_contents() 0 56 11
A str_word_count() 0 23 5
A strripos_in_byte() 0 12 4
A str_to_binary() 0 10 2
A symfony_polyfill_used() 0 16 5
B str_to_words() 0 36 8
A emoji_from_country_code() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like UTF8 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UTF8, and based on these observations, apply Extract Interface, too.

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

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

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

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

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

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

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

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

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

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

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

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