Passed
Push — master ( cbc452...0db427 )
by Lars
03:48
created

UTF8   F

Complexity

Total Complexity 1732

Size/Duplication

Total Lines 14003
Duplicated Lines 0 %

Test Coverage

Coverage 81.59%

Importance

Changes 103
Bugs 54 Features 6
Metric Value
eloc 4430
c 103
b 54
f 6
dl 0
loc 14003
ccs 3188
cts 3907
cp 0.8159
rs 0.8
wmc 1732

307 Methods

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

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

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

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

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

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

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

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

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

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

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

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