Completed
Push — master ( 695680...1c6b2b )
by Lars
09:15
created

UTF8   F

Complexity

Total Complexity 1712

Size/Duplication

Total Lines 13710
Duplicated Lines 0 %

Test Coverage

Coverage 79.76%

Importance

Changes 102
Bugs 53 Features 6
Metric Value
eloc 4383
dl 0
loc 13710
ccs 3081
cts 3863
cp 0.7976
rs 0.8
c 102
b 53
f 6
wmc 1712

301 Methods

Rating   Name   Duplication   Size   Complexity  
A char_at() 0 7 2
A __construct() 0 2 1
B chr_to_decimal() 0 38 8
A file_has_bom() 0 8 2
A max() 0 14 3
A add_bom_to_string() 0 7 2
A parse_str() 0 18 4
A filter_input() 0 16 3
A array_change_key_case() 0 23 5
A get_unique_string() 0 15 2
A is_bom() 0 10 3
A is_hexadecimal() 0 8 2
A encode_mimeheader() 0 25 5
A count_chars() 0 11 1
A ctype_loaded() 0 3 1
A has_uppercase() 0 8 2
A remove_left() 0 24 4
A max_chr_width() 0 8 2
A isBinary() 0 3 1
A ltrim() 0 27 5
A emoji_decode() 0 18 2
A is_utf8() 0 13 4
A remove_html() 0 3 1
A lcword() 0 13 1
A mbstring_loaded() 0 3 1
D chr() 0 109 18
A html_escape() 0 6 1
D normalize_encoding() 0 147 16
B get_file_type() 0 65 7
A chr_to_int() 0 3 1
C is_utf16() 0 71 16
A isHtml() 0 3 1
C filter() 0 59 13
A normalize_whitespace() 0 9 1
A isBase64() 0 3 1
A is_html() 0 14 2
A decode_mimeheader() 0 15 5
A html_decode() 0 6 1
A isUtf32() 0 3 1
A rtrim() 0 27 5
A regex_replace() 0 20 3
A chunk_split() 0 3 1
A replace_all() 0 11 2
A removeBOM() 0 3 1
A emoji_encode() 0 18 2
A is_alpha() 0 8 2
B get_random_string() 0 56 10
A fix_utf8() 0 30 4
A first_char() 0 14 4
A isUtf8() 0 3 1
A css_stripe_media_queries() 0 6 1
A clean() 0 48 6
A is_serialized() 0 11 3
A is_uppercase() 0 8 2
A is_ascii() 0 3 1
A normalize_line_ending() 0 3 1
D range() 0 71 23
B rawurldecode() 0 51 8
A normalize_msword() 0 3 1
A is_blank() 0 8 2
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 has_whitespace() 0 8 2
A pcre_utf8_support() 0 4 1
B between() 0 48 8
A codepoints() 0 36 5
A lowerCaseFirst() 0 13 1
A chr_map() 0 5 1
A cleanup() 0 25 2
A remove_right() 0 25 4
A remove_html_breaks() 0 3 1
A remove_invisible_characters() 0 9 1
A chars() 0 3 1
B is_binary() 0 38 9
A intlChar_loaded() 0 3 1
A lcfirst() 0 44 5
A finfo_loaded() 0 3 1
A fits_inside() 0 3 1
A is_binary_file() 0 16 3
A intl_loaded() 0 3 1
A html_stripe_empty_tags() 0 6 1
A chr_size_list() 0 17 3
A remove_bom() 0 22 5
F extract_text() 0 175 34
A json_loaded() 0 3 1
A isBom() 0 3 1
A int_to_chr() 0 3 1
A is_lowercase() 0 8 2
A hasBom() 0 3 1
A iconv_loaded() 0 3 1
A lcwords() 0 34 6
A isAscii() 0 3 1
A normalizeEncoding() 0 3 1
A filter_var() 0 15 2
A is_empty() 0 3 1
B html_encode() 0 53 11
A isUtf16() 0 3 1
F encode() 0 143 37
C is_utf32() 0 71 16
C ord() 0 77 16
A is_alphanumeric() 0 8 2
A json_decode() 0 14 2
A fix_simple_utf8() 0 32 4
A checkForSupport() 0 47 4
B is_json() 0 29 8
A int_to_hex() 0 7 2
A has_lowercase() 0 8 2
A json_encode() 0 10 2
A is_base64() 0 20 5
A hex_to_int() 0 14 3
A htmlentities() 0 28 3
A hex_to_chr() 0 3 1
A isJson() 0 3 1
A filter_input_array() 0 15 3
A getSupportInfo() 0 13 3
A replace_diamond_question_mark() 0 41 5
A chr_to_hex() 0 11 3
A min() 0 14 3
A collapse_whitespace() 0 8 2
C html_entity_decode() 0 58 13
A remove_duplicates() 0 16 4
A access() 0 11 4
B file_get_contents() 0 56 11
A callback() 0 3 1
A binary_to_str() 0 12 3
A bom() 0 3 1
A str_substr_after_first_separator() 0 28 6
A str_begins() 0 3 1
B str_camelize() 0 74 10
A str_contains() 0 10 2
B str_to_lines() 0 29 8
A substr_in_byte() 0 18 6
A strnatcasecmp() 0 5 1
A substr_left() 0 15 4
D strlen() 0 102 19
A str_isubstr_last() 0 25 4
A to_int() 0 7 2
A str_replace_beginning() 0 24 6
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
C utf8_decode() 0 61 13
B str_longest_common_suffix() 0 54 10
C wordwrap() 0 68 14
B ucfirst() 0 57 7
A str_pad_both() 0 12 1
A str_index_last() 0 11 1
A str_substr_last() 0 33 6
A str_limit() 0 26 6
A toUTF8() 0 3 1
A string() 0 12 3
B rxClass() 0 44 8
A str_ensure_right() 0 13 4
B str_titleize_for_humans() 0 170 7
A str_starts_with() 0 11 3
A str_humanize() 0 15 1
C substr_count_in_byte() 0 55 15
A strchr() 0 13 1
A strichr() 0 13 1
A str_index_first() 0 11 1
A strlen_in_byte() 0 12 3
A str_ireplace_ending() 0 21 6
C str_longest_common_substring() 0 76 16
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
D substr_replace() 0 124 27
A strstr_in_byte() 0 15 4
A str_matches_pattern() 0 3 1
C str_titleize() 0 69 12
A ws() 0 3 1
A str_replace_first() 0 20 2
A toLatin1() 0 3 1
A str_pad_right() 0 12 1
B ucwords() 0 51 9
A to_boolean() 0 35 5
C stristr() 0 71 15
A strncasecmp() 0 10 1
B strwidth() 0 43 8
A str_iends() 0 3 1
A trim() 0 27 5
A str_upper_camelize() 0 8 1
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 18 3
A to_latin1() 0 3 1
A str_replace_ending() 0 24 6
A string_has_bom() 0 10 3
B strtr() 0 34 8
B str_contains_all() 0 24 9
A str_isubstr_after_last_separator() 0 26 5
B strspn() 0 30 10
A strcasecmp() 0 21 1
A str_transliterate() 0 6 1
A str_ends() 0 3 1
B str_capitalize_name_helper() 0 82 10
A utf8_encode() 0 16 3
C str_detect_encoding() 0 111 13
A spaces_to_tabs() 0 11 3
A str_istarts_with() 0 11 3
A str_replace() 0 14 1
A substr_iright() 0 15 4
A to_iso8859() 0 16 4
A words_limit() 0 20 5
A strip_tags() 0 18 4
A str_isubstr_before_last_separator() 0 24 6
D str_truncate_safe() 0 78 18
A substr_right() 0 31 6
D str_split() 0 135 30
A str_ends_with_any() 0 13 4
A strrpos_in_byte() 0 12 4
F strrpos() 0 122 25
A showSupport() 0 17 3
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 strtocasefold() 0 33 7
A tabs_to_spaces() 0 11 3
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 to_ascii() 0 6 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 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
B str_snakeize() 0 57 6
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 toAscii() 0 6 1
A str_ibegins() 0 3 1
A str_capitalize_name() 0 8 1
B str_limit_after_word() 0 55 11
A str_upper_first() 0 13 1
A swapCase() 0 17 4
A substr_ileft() 0 15 4
A str_dasherize() 0 3 1
A str_ensure_left() 0 11 3
B urldecode_fix_win1252_chars() 0 227 1
A toIso8859() 0 3 1
B to_string() 0 26 7
A strtonatfold() 0 7 1
C strcspn() 0 52 12
A fixStrCaseHelper() 0 41 5
B str_split_pattern() 0 49 11
D strstr() 0 95 18
A str_isubstr_first() 0 25 4
A str_last_char() 0 16 4
A str_ireplace_beginning() 0 21 6
A str_substr_before_first_separator() 0 32 6
F substr() 0 146 32
A wordwrap_per_line() 0 28 5
A str_surround() 0 3 1
A strncmp() 0 19 4
A str_insert() 0 28 4
A utf8_fix_win1252_chars() 0 3 1
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 str_starts_with_any() 0 17 5
B strrichr() 0 54 11
A split() 0 6 1
A str_istarts_with_any() 0 17 5
B str_contains_any() 0 29 8
A initEmojiData() 0 26 4
B str_slice() 0 33 10
F strpos() 0 134 27
A str_shuffle() 0 35 6
A strcmp() 0 9 2
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

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

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

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

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

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