Passed
Push — master ( 0db427...c6fbed )
by Lars
04:28
created

UTF8   F

Complexity

Total Complexity 1736

Size/Duplication

Total Lines 14064
Duplicated Lines 0 %

Test Coverage

Coverage 81.66%

Importance

Changes 103
Bugs 54 Features 6
Metric Value
eloc 4444
dl 0
loc 14064
ccs 3202
cts 3921
cp 0.8166
rs 0.8
c 103
b 54
f 6
wmc 1736

308 Methods

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

How to fix   Complexity   

Complex Class

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

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

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

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

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

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

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

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

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