Passed
Push — master ( 099172...f96fe0 )
by Lars
04:30
created

UTF8   F

Complexity

Total Complexity 1727

Size/Duplication

Total Lines 13950
Duplicated Lines 0 %

Test Coverage

Coverage 81.43%

Importance

Changes 102
Bugs 53 Features 6
Metric Value
eloc 4417
c 102
b 53
f 6
dl 0
loc 13950
ccs 3170
cts 3893
cp 0.8143
rs 0.8
wmc 1727

306 Methods

Rating   Name   Duplication   Size   Complexity  
B chr_to_decimal() 0 38 8
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 add_bom_to_string() 0 7 2
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 array_change_key_case() 0 23 5
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 25 5
A substr_left() 0 15 4
A count_chars() 0 11 1
D strlen() 0 102 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 68 14
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
D chr() 0 109 18
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 44 8
B get_file_type() 0 65 7
A str_ensure_right() 0 13 4
A chr_to_int() 0 3 1
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 chunk_split() 0 3 1
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 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 clean() 0 48 6
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 18 3
A to_latin1() 0 3 1
A str_replace_ending() 0 24 6
A string_has_bom() 0 10 3
B strtr() 0 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 __construct() 0 2 1
A decimal_to_chr() 0 3 1
A to_iso8859() 0 16 4
A has_whitespace() 0 8 2
A words_limit() 0 20 5
A strip_tags() 0 18 4
A pcre_utf8_support() 0 4 1
B between() 0 48 8
A str_isubstr_before_last_separator() 0 24 6
D str_truncate_safe() 0 86 18
A codepoints() 0 36 5
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 chr_map() 0 5 1
A strrpos_in_byte() 0 12 4
A cleanup() 0 25 2
A remove_right() 0 25 4
F strrpos() 0 122 25
A remove_html_breaks() 0 3 1
A showSupport() 0 17 3
A char_at() 0 7 2
A remove_invisible_characters() 0 9 1
A single_chr_html_encode() 0 18 4
A chars() 0 3 1
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 chr_size_list() 0 17 3
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 143 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 checkForSupport() 0 47 4
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 3 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 41 5
A chr_to_hex() 0 11 3
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
A collapse_whitespace() 0 8 2
C html_entity_decode() 0 58 13
A str_starts_with_any() 0 17 5
B strrichr() 0 54 11
A split() 0 6 1
A str_istarts_with_any() 0 17 5
B str_contains_any() 0 29 8
A initEmojiData() 0 29 4
A remove_duplicates() 0 16 4
B str_slice() 0 33 10
A access() 0 11 4
F strpos() 0 134 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 callback() 0 3 1
A symfony_polyfill_used() 0 16 5
A binary_to_str() 0 12 3
A bom() 0 3 1
B str_to_words() 0 36 8

How to fix   Complexity   

Complex Class

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

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

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

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

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

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

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

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