Passed
Push — master ( 383245...eb36b7 )
by Lars
04:36 queued 14s
created

UTF8   F

Complexity

Total Complexity 1721

Size/Duplication

Total Lines 13792
Duplicated Lines 0 %

Test Coverage

Coverage 79.79%

Importance

Changes 102
Bugs 53 Features 6
Metric Value
eloc 4406
c 102
b 53
f 6
dl 0
loc 13792
ccs 3099
cts 3884
cp 0.7979
rs 0.8
wmc 1721

304 Methods

Rating   Name   Duplication   Size   Complexity  
A char_at() 0 7 2
A __construct() 0 2 1
B chr_to_decimal() 0 38 8
A file_has_bom() 0 8 2
A add_bom_to_string() 0 7 2
A filter_input() 0 16 3
A array_change_key_case() 0 23 5
A get_unique_string() 0 15 2
A encode_mimeheader() 0 25 5
A count_chars() 0 11 1
A ctype_loaded() 0 3 1
A has_uppercase() 0 8 2
A isBinary() 0 3 1
A emoji_decode() 0 18 2
D chr() 0 109 18
A html_escape() 0 6 1
B get_file_type() 0 65 7
A chr_to_int() 0 3 1
A isHtml() 0 3 1
C filter() 0 59 13
A isBase64() 0 3 1
A decode_mimeheader() 0 15 5
A html_decode() 0 6 1
A isUtf32() 0 3 1
A chunk_split() 0 3 1
A emoji_encode() 0 18 2
A is_alpha() 0 8 2
B get_random_string() 0 56 10
A fix_utf8() 0 30 4
A first_char() 0 14 4
A isUtf8() 0 3 1
A css_stripe_media_queries() 0 6 1
A clean() 0 48 6
D getCharDirection() 0 105 118
A htmlspecialchars() 0 15 3
A filter_var_array() 0 15 2
A decimal_to_chr() 0 3 1
A has_whitespace() 0 8 2
B between() 0 48 8
A codepoints() 0 36 5
A chr_map() 0 5 1
A cleanup() 0 25 2
A chars() 0 3 1
A intlChar_loaded() 0 3 1
A finfo_loaded() 0 3 1
A fits_inside() 0 3 1
A intl_loaded() 0 3 1
A html_stripe_empty_tags() 0 6 1
A chr_size_list() 0 17 3
F extract_text() 0 175 34
A isBom() 0 3 1
A int_to_chr() 0 3 1
A hasBom() 0 3 1
A iconv_loaded() 0 3 1
A isAscii() 0 3 1
A filter_var() 0 15 2
B html_encode() 0 53 11
A isUtf16() 0 3 1
F encode() 0 143 37
A is_alphanumeric() 0 8 2
A fix_simple_utf8() 0 32 4
A checkForSupport() 0 47 4
A int_to_hex() 0 7 2
A has_lowercase() 0 8 2
A hex_to_int() 0 14 3
A htmlentities() 0 28 3
A hex_to_chr() 0 3 1
A isJson() 0 3 1
A filter_input_array() 0 15 3
A getSupportInfo() 0 13 3
A chr_to_hex() 0 11 3
A collapse_whitespace() 0 8 2
C html_entity_decode() 0 58 13
A access() 0 11 4
B file_get_contents() 0 56 11
A callback() 0 3 1
A binary_to_str() 0 12 3
A bom() 0 3 1
A is_bom() 0 10 3
A is_hexadecimal() 0 8 2
A is_html() 0 14 2
A is_ascii() 0 3 1
A is_blank() 0 8 2
B is_binary() 0 38 9
A is_binary_file() 0 16 3
A is_empty() 0 3 1
A is_printable() 0 3 1
A is_base64() 0 20 5
A is_punctuation() 0 3 1
A str_substr_after_first_separator() 0 28 6
A str_begins() 0 3 1
A max() 0 14 3
B str_camelize() 0 74 10
A parse_str() 0 18 4
A str_contains() 0 10 2
B str_to_lines() 0 29 8
A substr_in_byte() 0 18 6
A strnatcasecmp() 0 5 1
A substr_left() 0 15 4
D strlen() 0 102 19
A str_isubstr_last() 0 25 4
A to_int() 0 7 2
A str_replace_beginning() 0 24 6
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
C utf8_decode() 0 61 13
A ltrim() 0 27 5
A is_utf8() 0 13 4
A remove_html() 0 3 1
B str_longest_common_suffix() 0 54 10
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
A toUTF8() 0 3 1
A string() 0 12 3
D normalize_encoding() 0 147 16
B rxClass() 0 44 8
A str_ensure_right() 0 13 4
B str_titleize_for_humans() 0 170 7
C is_utf16() 0 71 16
A normalize_whitespace() 0 9 1
A str_starts_with() 0 11 3
A str_humanize() 0 15 1
C substr_count_in_byte() 0 55 15
A strchr() 0 13 1
A strichr() 0 13 1
A str_index_first() 0 11 1
A strlen_in_byte() 0 12 3
A str_ireplace_ending() 0 21 6
A rtrim() 0 27 5
C str_longest_common_substring() 0 76 16
A regex_replace() 0 20 3
A titlecase() 0 35 5
A getData() 0 6 1
A str_iindex_first() 0 11 1
B strtolower() 0 60 10
B urldecode() 0 51 8
A str_isubstr_before_first_separator() 0 19 5
B strrev() 0 43 10
A replace_all() 0 11 2
D substr_replace() 0 124 27
A removeBOM() 0 3 1
A strstr_in_byte() 0 15 4
A str_matches_pattern() 0 3 1
C str_titleize() 0 69 12
A ws() 0 3 1
A str_replace_first() 0 20 2
A toLatin1() 0 3 1
A str_pad_right() 0 12 1
B ucwords() 0 51 9
A to_boolean() 0 35 5
C stristr() 0 71 15
A strncasecmp() 0 10 1
B strwidth() 0 43 8
A str_iends() 0 3 1
A trim() 0 27 5
A 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 34 8
B str_contains_all() 0 24 9
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 82 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 str_replace() 0 14 1
A substr_iright() 0 15 4
A replace() 0 11 2
A to_iso8859() 0 16 4
A words_limit() 0 20 5
A strip_tags() 0 18 4
A pcre_utf8_support() 0 4 1
A str_isubstr_before_last_separator() 0 24 6
D str_truncate_safe() 0 78 18
A substr_right() 0 31 6
A lowerCaseFirst() 0 13 1
D str_split() 0 135 30
A str_ends_with_any() 0 13 4
A strrpos_in_byte() 0 12 4
A remove_right() 0 25 4
F strrpos() 0 122 25
A remove_html_breaks() 0 3 1
A showSupport() 0 17 3
A remove_invisible_characters() 0 9 1
A single_chr_html_encode() 0 18 4
A str_replace_last() 0 19 2
A str_iindex_last() 0 11 1
A str_substr_before_last_separator() 0 31 6
B strtocasefold() 0 33 7
A lcfirst() 0 44 5
A tabs_to_spaces() 0 11 3
B is_url() 0 41 7
B str_truncate() 0 44 7
D strripos() 0 99 19
A strpos_in_byte() 0 12 4
A str_ends_with() 0 11 3
A to_ascii() 0 6 1
A reduce_string_array() 0 29 6
B str_longest_common_prefix() 0 51 8
A mbstring_overloaded() 0 11 2
A str_pad_left() 0 12 1
A str_substr_first() 0 33 6
A 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
A json_loaded() 0 3 1
B str_snakeize() 0 57 6
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 toAscii() 0 6 1
A str_ibegins() 0 3 1
A str_capitalize_name() 0 8 1
B str_limit_after_word() 0 55 11
A lcwords() 0 34 6
A str_upper_first() 0 13 1
A normalizeEncoding() 0 3 1
A swapCase() 0 17 4
A substr_ileft() 0 15 4
A str_dasherize() 0 3 1
A str_ensure_left() 0 11 3
B urldecode_fix_win1252_chars() 0 227 1
A toIso8859() 0 3 1
C is_utf32() 0 71 16
C ord() 0 77 16
B to_string() 0 26 7
A strtonatfold() 0 7 1
A json_decode() 0 14 2
C strcspn() 0 52 12
B is_json() 0 29 8
A fixStrCaseHelper() 0 41 5
B str_split_pattern() 0 49 11
D strstr() 0 95 18
A json_encode() 0 10 2
A str_isubstr_first() 0 25 4
A str_last_char() 0 16 4
A str_ireplace_beginning() 0 21 6
A str_substr_before_first_separator() 0 32 6
F substr() 0 144 32
A wordwrap_per_line() 0 28 5
A str_surround() 0 3 1
A strncmp() 0 19 4
A str_insert() 0 28 4
A utf8_fix_win1252_chars() 0 3 1
A replace_diamond_question_mark() 0 41 5
D is_utf8_string() 0 134 28
A to_utf8_convert_helper() 0 28 5
B str_delimit() 0 33 8
B strtoupper() 0 60 10
A min() 0 14 3
A str_starts_with_any() 0 17 5
B strrichr() 0 54 11
A split() 0 6 1
A str_istarts_with_any() 0 17 5
B str_contains_any() 0 29 8
A initEmojiData() 0 26 4
A remove_duplicates() 0 16 4
B str_slice() 0 33 10
F strpos() 0 134 27
A str_shuffle() 0 35 6
A strcmp() 0 9 2
A str_word_count() 0 23 5
A strripos_in_byte() 0 12 4
A str_to_binary() 0 10 2
A symfony_polyfill_used() 0 16 5
B str_to_words() 0 36 8

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\helper;
6
7
/**
8
 * @psalm-immutable
9
 */
10
final class UTF8
11
{
12
    /**
13
     * (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control])
14
     * This regular expression is a work around for http://bugs.exim.org/1279
15
     */
16
    const GRAPHEME_CLUSTER_RX = "(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])";
17
18
    /**
19
     * Bom => Byte-Length
20
     *
21
     * INFO: https://en.wikipedia.org/wiki/Byte_order_mark
22
     *
23
     * @var array<string, int>
24
     */
25
    private static $BOM = [
26
        "\xef\xbb\xbf"     => 3, // UTF-8 BOM
27
        ''              => 6, // UTF-8 BOM as "WINDOWS-1252" (one char has [maybe] more then one byte ...)
28
        "\x00\x00\xfe\xff" => 4, // UTF-32 (BE) BOM
29
        '  þÿ'             => 6, // UTF-32 (BE) BOM as "WINDOWS-1252"
30
        "\xff\xfe\x00\x00" => 4, // UTF-32 (LE) BOM
31
        'ÿþ  '             => 6, // UTF-32 (LE) BOM as "WINDOWS-1252"
32
        "\xfe\xff"         => 2, // UTF-16 (BE) BOM
33
        'þÿ'               => 4, // UTF-16 (BE) BOM as "WINDOWS-1252"
34
        "\xff\xfe"         => 2, // UTF-16 (LE) BOM
35
        'ÿþ'               => 4, // UTF-16 (LE) BOM as "WINDOWS-1252"
36
    ];
37
38
    /**
39
     * Numeric code point => UTF-8 Character
40
     *
41
     * url: http://www.w3schools.com/charsets/ref_utf_punctuation.asp
42
     *
43
     * @var array<int, string>
44
     */
45
    private static $WHITESPACE = [
46
        // NUL Byte
47
        0 => "\x0",
48
        // Tab
49
        9 => "\x9",
50
        // New Line
51
        10 => "\xa",
52
        // Vertical Tab
53
        11 => "\xb",
54
        // Carriage Return
55
        13 => "\xd",
56
        // Ordinary Space
57
        32 => "\x20",
58
        // NO-BREAK SPACE
59
        160 => "\xc2\xa0",
60
        // OGHAM SPACE MARK
61
        5760 => "\xe1\x9a\x80",
62
        // MONGOLIAN VOWEL SEPARATOR
63
        6158 => "\xe1\xa0\x8e",
64
        // EN QUAD
65
        8192 => "\xe2\x80\x80",
66
        // EM QUAD
67
        8193 => "\xe2\x80\x81",
68
        // EN SPACE
69
        8194 => "\xe2\x80\x82",
70
        // EM SPACE
71
        8195 => "\xe2\x80\x83",
72
        // THREE-PER-EM SPACE
73
        8196 => "\xe2\x80\x84",
74
        // FOUR-PER-EM SPACE
75
        8197 => "\xe2\x80\x85",
76
        // SIX-PER-EM SPACE
77
        8198 => "\xe2\x80\x86",
78
        // FIGURE SPACE
79
        8199 => "\xe2\x80\x87",
80
        // PUNCTUATION SPACE
81
        8200 => "\xe2\x80\x88",
82
        // THIN SPACE
83
        8201 => "\xe2\x80\x89",
84
        //HAIR SPACE
85
        8202 => "\xe2\x80\x8a",
86
        // LINE SEPARATOR
87
        8232 => "\xe2\x80\xa8",
88
        // PARAGRAPH SEPARATOR
89
        8233 => "\xe2\x80\xa9",
90
        // NARROW NO-BREAK SPACE
91
        8239 => "\xe2\x80\xaf",
92
        // MEDIUM MATHEMATICAL SPACE
93
        8287 => "\xe2\x81\x9f",
94
        // HALFWIDTH HANGUL FILLER
95
        65440 => "\xef\xbe\xa0",
96
        // IDEOGRAPHIC SPACE
97
        12288 => "\xe3\x80\x80",
98
    ];
99
100
    /**
101
     * @var array<string, string>
102
     */
103
    private static $WHITESPACE_TABLE = [
104
        'SPACE'                     => "\x20",
105
        'NO-BREAK SPACE'            => "\xc2\xa0",
106
        'OGHAM SPACE MARK'          => "\xe1\x9a\x80",
107
        'EN QUAD'                   => "\xe2\x80\x80",
108
        'EM QUAD'                   => "\xe2\x80\x81",
109
        'EN SPACE'                  => "\xe2\x80\x82",
110
        'EM SPACE'                  => "\xe2\x80\x83",
111
        'THREE-PER-EM SPACE'        => "\xe2\x80\x84",
112
        'FOUR-PER-EM SPACE'         => "\xe2\x80\x85",
113
        'SIX-PER-EM SPACE'          => "\xe2\x80\x86",
114
        'FIGURE SPACE'              => "\xe2\x80\x87",
115
        'PUNCTUATION SPACE'         => "\xe2\x80\x88",
116
        'THIN SPACE'                => "\xe2\x80\x89",
117
        'HAIR SPACE'                => "\xe2\x80\x8a",
118
        'LINE SEPARATOR'            => "\xe2\x80\xa8",
119
        'PARAGRAPH SEPARATOR'       => "\xe2\x80\xa9",
120
        'ZERO WIDTH SPACE'          => "\xe2\x80\x8b",
121
        'NARROW NO-BREAK SPACE'     => "\xe2\x80\xaf",
122
        'MEDIUM MATHEMATICAL SPACE' => "\xe2\x81\x9f",
123
        'IDEOGRAPHIC SPACE'         => "\xe3\x80\x80",
124
        'HALFWIDTH HANGUL FILLER'   => "\xef\xbe\xa0",
125
    ];
126
127
    /**
128
     * @var array{upper: string[], lower: string[]}
129
     */
130
    private static $COMMON_CASE_FOLD = [
131
        'upper' => [
132
            'µ',
133
            'ſ',
134
            "\xCD\x85",
135
            'ς',
136
            'ẞ',
137
            "\xCF\x90",
138
            "\xCF\x91",
139
            "\xCF\x95",
140
            "\xCF\x96",
141
            "\xCF\xB0",
142
            "\xCF\xB1",
143
            "\xCF\xB5",
144
            "\xE1\xBA\x9B",
145
            "\xE1\xBE\xBE",
146
        ],
147
        'lower' => [
148
            'μ',
149
            's',
150
            'ι',
151
            'σ',
152
            'ß',
153
            'β',
154
            'θ',
155
            'φ',
156
            'π',
157
            'κ',
158
            'ρ',
159
            'ε',
160
            "\xE1\xB9\xA1",
161
            'ι',
162
        ],
163
    ];
164
165
    /**
166
     * @var array<string, mixed>
167
     */
168
    private static $SUPPORT = [];
169
170
    /**
171
     * @var array<string, string>|null
172
     */
173
    private static $BROKEN_UTF8_FIX;
174
175
    /**
176
     * @var array<int, string>|null
177
     */
178
    private static $WIN1252_TO_UTF8;
179
180
    /**
181
     * @var array<int ,string>|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int at position 2 could not be parsed: Expected '>' at position 2, but found 'int'.
Loading history...
182
     */
183
    private static $INTL_TRANSLITERATOR_LIST;
184
185
    /**
186
     * @var array<string>|null
187
     */
188
    private static $ENCODINGS;
189
190
    /**
191
     * @var array<string ,int>|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string at position 2 could not be parsed: Expected '>' at position 2, but found 'string'.
Loading history...
192
     */
193
    private static $ORD;
194
195
    /**
196
     * @var array<string, string>|null
197
     */
198
    private static $EMOJI;
199
200
    /**
201
     * @var array<string>|null
202
     */
203
    private static $EMOJI_VALUES_CACHE;
204
205
    /**
206
     * @var array<string>|null
207
     */
208
    private static $EMOJI_KEYS_CACHE;
209
210
    /**
211
     * @var array<string>|null
212
     */
213
    private static $EMOJI_KEYS_REVERSIBLE_CACHE;
214
215
    /**
216
     * @var array<int, string>|null
217
     */
218
    private static $CHR;
219
220
    /**
221
     * __construct()
222
     */
223 34
    public function __construct()
224
    {
225 34
    }
226
227
    /**
228
     * Return the character at the specified position: $str[1] like functionality.
229
     *
230
     * @param string $str      <p>A UTF-8 string.</p>
231
     * @param int    $pos      <p>The position of character to return.</p>
232
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
233
     *
234
     * @psalm-pure
235
     *
236
     * @return string single multi-byte character
237
     */
238 3
    public static function access(string $str, int $pos, string $encoding = 'UTF-8'): string
239
    {
240 3
        if ($str === '' || $pos < 0) {
241 2
            return '';
242
        }
243
244 3
        if ($encoding === 'UTF-8') {
245 3
            return (string) \mb_substr($str, $pos, 1);
246
        }
247
248
        return (string) self::substr($str, $pos, 1, $encoding);
249
    }
250
251
    /**
252
     * Prepends UTF-8 BOM character to the string and returns the whole string.
253
     *
254
     * INFO: If BOM already existed there, the Input string is returned.
255
     *
256
     * @param string $str <p>The input string.</p>
257
     *
258
     * @psalm-pure
259
     *
260
     * @return string the output string that contains BOM
261
     */
262 2
    public static function add_bom_to_string(string $str): string
263
    {
264 2
        if (self::string_has_bom($str) === false) {
265 2
            $str = self::bom() . $str;
266
        }
267
268 2
        return $str;
269
    }
270
271
    /**
272
     * Changes all keys in an array.
273
     *
274
     * @param array<string, mixed> $array    <p>The array to work on</p>
275
     * @param int                  $case     [optional] <p> Either <strong>CASE_UPPER</strong><br>
276
     *                                       or <strong>CASE_LOWER</strong> (default)</p>
277
     * @param string               $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
278
     *
279
     * @psalm-pure
280
     *
281
     * @return string[]
282
     *                  <p>An array with its keys lower- or uppercased.</p>
283
     */
284 2
    public static function array_change_key_case(
285
        array $array,
286
        int $case = \CASE_LOWER,
287
        string $encoding = 'UTF-8'
288
    ): array {
289
        if (
290 2
            $case !== \CASE_LOWER
291
            &&
292 2
            $case !== \CASE_UPPER
293
        ) {
294
            $case = \CASE_LOWER;
295
        }
296
297 2
        $return = [];
298 2
        foreach ($array as $key => &$value) {
299 2
            $key = $case === \CASE_LOWER
300 2
                ? self::strtolower((string) $key, $encoding)
301 2
                : self::strtoupper((string) $key, $encoding);
302
303 2
            $return[$key] = $value;
304
        }
305
306 2
        return $return;
307
    }
308
309
    /**
310
     * Returns the substring between $start and $end, if found, or an empty
311
     * string. An optional offset may be supplied from which to begin the
312
     * search for the start string.
313
     *
314
     * @param string $str
315
     * @param string $start    <p>Delimiter marking the start of the substring.</p>
316
     * @param string $end      <p>Delimiter marking the end of the substring.</p>
317
     * @param int    $offset   [optional] <p>Index from which to begin the search. Default: 0</p>
318
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
319
     *
320
     * @psalm-pure
321
     *
322
     * @return string
323
     */
324 16
    public static function between(
325
        string $str,
326
        string $start,
327
        string $end,
328
        int $offset = 0,
329
        string $encoding = 'UTF-8'
330
    ): string {
331 16
        if ($encoding === 'UTF-8') {
332 8
            $start_position = \mb_strpos($str, $start, $offset);
333 8
            if ($start_position === false) {
334 1
                return '';
335
            }
336
337 7
            $substr_index = $start_position + (int) \mb_strlen($start);
338 7
            $end_position = \mb_strpos($str, $end, $substr_index);
339
            if (
340 7
                $end_position === false
341
                ||
342 7
                $end_position === $substr_index
343
            ) {
344 2
                return '';
345
            }
346
347 5
            return (string) \mb_substr($str, $substr_index, $end_position - $substr_index);
348
        }
349
350 8
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
351
352 8
        $start_position = self::strpos($str, $start, $offset, $encoding);
353 8
        if ($start_position === false) {
354 1
            return '';
355
        }
356
357 7
        $substr_index = $start_position + (int) self::strlen($start, $encoding);
358 7
        $end_position = self::strpos($str, $end, $substr_index, $encoding);
359
        if (
360 7
            $end_position === false
361
            ||
362 7
            $end_position === $substr_index
363
        ) {
364 2
            return '';
365
        }
366
367 5
        return (string) self::substr(
368 5
            $str,
369 5
            $substr_index,
370 5
            $end_position - $substr_index,
371 5
            $encoding
372
        );
373
    }
374
375
    /**
376
     * Convert binary into a string.
377
     *
378
     * @param mixed $bin 1|0
379
     *
380
     * @psalm-pure
381
     *
382
     * @return string
383
     */
384 2
    public static function binary_to_str($bin): string
385
    {
386 2
        if (!isset($bin[0])) {
387
            return '';
388
        }
389
390 2
        $convert = \base_convert($bin, 2, 16);
391 2
        if ($convert === '0') {
392 1
            return '';
393
        }
394
395 2
        return \pack('H*', $convert);
396
    }
397
398
    /**
399
     * Returns the UTF-8 Byte Order Mark Character.
400
     *
401
     * INFO: take a look at UTF8::$bom for e.g. UTF-16 and UTF-32 BOM values
402
     *
403
     * @psalm-pure
404
     *
405
     * @return string UTF-8 Byte Order Mark
406
     */
407 4
    public static function bom(): string
408
    {
409 4
        return "\xef\xbb\xbf";
410
    }
411
412
    /**
413
     * @alias of UTF8::chr_map()
414
     *
415
     * @param callable $callback
416
     * @param string   $str
417
     *
418
     * @psalm-pure
419
     *
420
     * @return string[]
421
     *
422
     * @see   UTF8::chr_map()
423
     */
424 2
    public static function callback($callback, string $str): array
425
    {
426 2
        return self::chr_map($callback, $str);
427
    }
428
429
    /**
430
     * Returns the character at $index, with indexes starting at 0.
431
     *
432
     * @param string $str      <p>The input string.</p>
433
     * @param int    $index    <p>Position of the character.</p>
434
     * @param string $encoding [optional] <p>Default is UTF-8</p>
435
     *
436
     * @psalm-pure
437
     *
438
     * @return string the character at $index
439
     */
440 9
    public static function char_at(string $str, int $index, string $encoding = 'UTF-8'): string
441
    {
442 9
        if ($encoding === 'UTF-8') {
443 5
            return (string) \mb_substr($str, $index, 1);
444
        }
445
446 4
        return (string) self::substr($str, $index, 1, $encoding);
447
    }
448
449
    /**
450
     * Returns an array consisting of the characters in the string.
451
     *
452
     * @param string $str <p>The input string.</p>
453
     *
454
     * @psalm-pure
455
     *
456
     * @return string[] an array of chars
457
     */
458 3
    public static function chars(string $str): array
459
    {
460 3
        return self::str_split($str);
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::str_split($str) returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
461
    }
462
463
    /**
464
     * This method will auto-detect your server environment for UTF-8 support.
465
     *
466
     * @return true|null
467
     *
468
     * @internal <p>You don't need to run it manually, it will be triggered if it's needed.</p>
469
     */
470 5
    public static function checkForSupport()
471
    {
472 5
        if (!isset(self::$SUPPORT['already_checked_via_portable_utf8'])) {
473
            self::$SUPPORT['already_checked_via_portable_utf8'] = true;
474
475
            // http://php.net/manual/en/book.mbstring.php
476
            self::$SUPPORT['mbstring'] = self::mbstring_loaded();
477
            self::$SUPPORT['mbstring_func_overload'] = self::mbstring_overloaded();
478
            if (self::$SUPPORT['mbstring'] === true) {
479
                \mb_internal_encoding('UTF-8');
480
                /** @noinspection UnusedFunctionResultInspection */
481
                /** @noinspection PhpComposerExtensionStubsInspection */
482
                \mb_regex_encoding('UTF-8');
483
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
484
            }
485
486
            // http://php.net/manual/en/book.iconv.php
487
            self::$SUPPORT['iconv'] = self::iconv_loaded();
488
489
            // http://php.net/manual/en/book.intl.php
490
            self::$SUPPORT['intl'] = self::intl_loaded();
491
492
            // http://php.net/manual/en/class.intlchar.php
493
            self::$SUPPORT['intlChar'] = self::intlChar_loaded();
494
495
            // http://php.net/manual/en/book.ctype.php
496
            self::$SUPPORT['ctype'] = self::ctype_loaded();
497
498
            // http://php.net/manual/en/class.finfo.php
499
            self::$SUPPORT['finfo'] = self::finfo_loaded();
500
501
            // http://php.net/manual/en/book.json.php
502
            self::$SUPPORT['json'] = self::json_loaded();
503
504
            // http://php.net/manual/en/book.pcre.php
505
            self::$SUPPORT['pcre_utf8'] = self::pcre_utf8_support();
506
507
            self::$SUPPORT['symfony_polyfill_used'] = self::symfony_polyfill_used();
508
            if (self::$SUPPORT['symfony_polyfill_used'] === true) {
509
                \mb_internal_encoding('UTF-8');
510
                self::$SUPPORT['mbstring_internal_encoding'] = 'UTF-8';
511
            }
512
513
            return true;
514
        }
515
516 5
        return null;
517
    }
518
519
    /**
520
     * Generates a UTF-8 encoded character from the given code point.
521
     *
522
     * INFO: opposite to UTF8::ord()
523
     *
524
     * @param int|string $code_point <p>The code point for which to generate a character.</p>
525
     * @param string     $encoding   [optional] <p>Default is UTF-8</p>
526
     *
527
     * @psalm-pure
528
     *
529
     * @return string|null multi-byte character, returns null on failure or empty input
530
     */
531 21
    public static function chr($code_point, string $encoding = 'UTF-8')
532
    {
533
        // init
534
        /**
535
         * @psalm-suppress ImpureStaticVariable
536
         *
537
         * @var array<string,string>
538
         */
539 21
        static $CHAR_CACHE = [];
540
541 21
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
542 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
543
        }
544
545
        if (
546 21
            $encoding !== 'UTF-8'
547
            &&
548 21
            $encoding !== 'ISO-8859-1'
549
            &&
550 21
            $encoding !== 'WINDOWS-1252'
551
            &&
552 21
            self::$SUPPORT['mbstring'] === false
553
        ) {
554
            /**
555
             * @psalm-suppress ImpureFunctionCall - is is only a warning
556
             */
557
            \trigger_error('UTF8::chr() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
558
        }
559
560 21
        $cache_key = $code_point . '_' . $encoding;
561 21
        if (isset($CHAR_CACHE[$cache_key]) === true) {
562 19
            return $CHAR_CACHE[$cache_key];
563
        }
564
565 12
        if ($code_point <= 127) { // use "simple"-char only until "\x80"
566
567 12
            if (self::$CHR === null) {
568
                self::$CHR = self::getData('chr');
569
            }
570
571
            /**
572
             * @psalm-suppress PossiblyNullArrayAccess
573
             */
574 12
            $chr = self::$CHR[$code_point];
575
576 12
            if ($encoding !== 'UTF-8') {
577 1
                $chr = self::encode($encoding, $chr);
578
            }
579
580 12
            return $CHAR_CACHE[$cache_key] = $chr;
581
        }
582
583
        //
584
        // fallback via "IntlChar"
585
        //
586
587 5
        if (self::$SUPPORT['intlChar'] === true) {
588
            /** @noinspection PhpComposerExtensionStubsInspection */
589 5
            $chr = \IntlChar::chr($code_point);
590
591 5
            if ($encoding !== 'UTF-8') {
592
                $chr = self::encode($encoding, $chr);
593
            }
594
595 5
            return $CHAR_CACHE[$cache_key] = $chr;
596
        }
597
598
        //
599
        // fallback via vanilla php
600
        //
601
602
        if (self::$CHR === null) {
603
            self::$CHR = self::getData('chr');
604
        }
605
606
        $code_point = (int) $code_point;
607
        if ($code_point <= 0x7F) {
608
            /**
609
             * @psalm-suppress PossiblyNullArrayAccess
610
             */
611
            $chr = self::$CHR[$code_point];
612
        } elseif ($code_point <= 0x7FF) {
613
            /**
614
             * @psalm-suppress PossiblyNullArrayAccess
615
             */
616
            $chr = self::$CHR[($code_point >> 6) + 0xC0] .
617
                   self::$CHR[($code_point & 0x3F) + 0x80];
618
        } elseif ($code_point <= 0xFFFF) {
619
            /**
620
             * @psalm-suppress PossiblyNullArrayAccess
621
             */
622
            $chr = self::$CHR[($code_point >> 12) + 0xE0] .
623
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
624
                   self::$CHR[($code_point & 0x3F) + 0x80];
625
        } else {
626
            /**
627
             * @psalm-suppress PossiblyNullArrayAccess
628
             */
629
            $chr = self::$CHR[($code_point >> 18) + 0xF0] .
630
                   self::$CHR[(($code_point >> 12) & 0x3F) + 0x80] .
631
                   self::$CHR[(($code_point >> 6) & 0x3F) + 0x80] .
632
                   self::$CHR[($code_point & 0x3F) + 0x80];
633
        }
634
635
        if ($encoding !== 'UTF-8') {
636
            $chr = self::encode($encoding, $chr);
637
        }
638
639
        return $CHAR_CACHE[$cache_key] = $chr;
640
    }
641
642
    /**
643
     * Applies callback to all characters of a string.
644
     *
645
     * @param callable $callback <p>The callback function.</p>
646
     * @param string   $str      <p>UTF-8 string to run callback on.</p>
647
     *
648
     * @psalm-pure
649
     *
650
     * @return string[]
651
     *                  <p>The outcome of the callback, as array.</p>
652
     */
653 2
    public static function chr_map($callback, string $str): array
654
    {
655 2
        return \array_map(
656 2
            $callback,
657 2
            self::str_split($str)
658
        );
659
    }
660
661
    /**
662
     * Generates an array of byte length of each character of a Unicode string.
663
     *
664
     * 1 byte => U+0000  - U+007F
665
     * 2 byte => U+0080  - U+07FF
666
     * 3 byte => U+0800  - U+FFFF
667
     * 4 byte => U+10000 - U+10FFFF
668
     *
669
     * @param string $str <p>The original unicode string.</p>
670
     *
671
     * @psalm-pure
672
     *
673
     * @return int[] an array of byte lengths of each character
674
     */
675 4
    public static function chr_size_list(string $str): array
676
    {
677 4
        if ($str === '') {
678 4
            return [];
679
        }
680
681 4
        if (self::$SUPPORT['mbstring_func_overload'] === true) {
682
            return \array_map(
683
                static function (string $data): int {
684
                    // "mb_" is available if overload is used, so use it ...
685
                    return \mb_strlen($data, 'CP850'); // 8-BIT
686
                },
687
                self::str_split($str)
688
            );
689
        }
690
691 4
        return \array_map('\strlen', self::str_split($str));
692
    }
693
694
    /**
695
     * Get a decimal code representation of a specific character.
696
     *
697
     * @param string $char <p>The input character.</p>
698
     *
699
     * @psalm-pure
700
     *
701
     * @return int
702
     */
703 4
    public static function chr_to_decimal(string $char): int
704
    {
705 4
        if (self::$SUPPORT['iconv'] === true) {
706 4
            $chr_tmp = \iconv('UTF-8', 'UCS-4LE', $char);
707 4
            if ($chr_tmp !== false) {
708
                /** @noinspection OffsetOperationsInspection */
709 4
                return \unpack('V', $chr_tmp)[1];
710
            }
711
        }
712
713
        $code = self::ord($char[0]);
714
        $bytes = 1;
715
716
        if (!($code & 0x80)) {
717
            // 0xxxxxxx
718
            return $code;
719
        }
720
721
        if (($code & 0xe0) === 0xc0) {
722
            // 110xxxxx
723
            $bytes = 2;
724
            $code &= ~0xc0;
725
        } elseif (($code & 0xf0) === 0xe0) {
726
            // 1110xxxx
727
            $bytes = 3;
728
            $code &= ~0xe0;
729
        } elseif (($code & 0xf8) === 0xf0) {
730
            // 11110xxx
731
            $bytes = 4;
732
            $code &= ~0xf0;
733
        }
734
735
        for ($i = 2; $i <= $bytes; ++$i) {
736
            // 10xxxxxx
737
            $code = ($code << 6) + (self::ord($char[$i - 1]) & ~0x80);
738
        }
739
740
        return $code;
741
    }
742
743
    /**
744
     * Get hexadecimal code point (U+xxxx) of a UTF-8 encoded character.
745
     *
746
     * @param int|string $char   <p>The input character</p>
747
     * @param string     $prefix [optional]
748
     *
749
     * @psalm-pure
750
     *
751
     * @return string The code point encoded as U+xxxx
752
     */
753 2
    public static function chr_to_hex($char, string $prefix = 'U+'): string
754
    {
755 2
        if ($char === '') {
756 2
            return '';
757
        }
758
759 2
        if ($char === '&#0;') {
760 2
            $char = '';
761
        }
762
763 2
        return self::int_to_hex(self::ord((string) $char), $prefix);
764
    }
765
766
    /**
767
     * alias for "UTF8::chr_to_decimal()"
768
     *
769
     * @param string $chr
770
     *
771
     * @psalm-pure
772
     *
773
     * @return int
774
     *
775
     * @see        UTF8::chr_to_decimal()
776
     * @deprecated <p>please use "UTF8::chr_to_decimal()"</p>
777
     */
778 2
    public static function chr_to_int(string $chr): int
779
    {
780 2
        return self::chr_to_decimal($chr);
781
    }
782
783
    /**
784
     * Splits a string into smaller chunks and multiple lines, using the specified line ending character.
785
     *
786
     * @param string $body         <p>The original string to be split.</p>
787
     * @param int    $chunk_length [optional] <p>The maximum character length of a chunk.</p>
788
     * @param string $end          [optional] <p>The character(s) to be inserted at the end of each chunk.</p>
789
     *
790
     * @psalm-pure
791
     *
792
     * @return string the chunked string
793
     */
794 4
    public static function chunk_split(string $body, int $chunk_length = 76, string $end = "\r\n"): string
795
    {
796 4
        return \implode($end, self::str_split($body, $chunk_length));
797
    }
798
799
    /**
800
     * Accepts a string and removes all non-UTF-8 characters from it + extras if needed.
801
     *
802
     * @param string $str                                     <p>The string to be sanitized.</p>
803
     * @param bool   $remove_bom                              [optional] <p>Set to true, if you need to remove
804
     *                                                        UTF-BOM.</p>
805
     * @param bool   $normalize_whitespace                    [optional] <p>Set to true, if you need to normalize the
806
     *                                                        whitespace.</p>
807
     * @param bool   $normalize_msword                        [optional] <p>Set to true, if you need to normalize MS
808
     *                                                        Word chars e.g.: "…"
809
     *                                                        => "..."</p>
810
     * @param bool   $keep_non_breaking_space                 [optional] <p>Set to true, to keep non-breaking-spaces,
811
     *                                                        in
812
     *                                                        combination with
813
     *                                                        $normalize_whitespace</p>
814
     * @param bool   $replace_diamond_question_mark           [optional] <p>Set to true, if you need to remove diamond
815
     *                                                        question mark e.g.: "�"</p>
816
     * @param bool   $remove_invisible_characters             [optional] <p>Set to false, if you not want to remove
817
     *                                                        invisible characters e.g.: "\0"</p>
818
     * @param bool   $remove_invisible_characters_url_encoded [optional] <p>Set to true, if you not want to remove
819
     *                                                        invisible url encoded characters e.g.: "%0B"<br> WARNING:
820
     *                                                        maybe contains false-positives e.g. aa%0Baa -> aaaa.
821
     *                                                        </p>
822
     *
823
     * @psalm-pure
824
     *
825
     * @return string clean UTF-8 encoded string
826
     */
827 87
    public static function clean(
828
        string $str,
829
        bool $remove_bom = false,
830
        bool $normalize_whitespace = false,
831
        bool $normalize_msword = false,
832
        bool $keep_non_breaking_space = false,
833
        bool $replace_diamond_question_mark = false,
834
        bool $remove_invisible_characters = true,
835
        bool $remove_invisible_characters_url_encoded = false
836
    ): string {
837
        // http://stackoverflow.com/questions/1401317/remove-non-utf8-characters-from-string
838
        // caused connection reset problem on larger strings
839
840 87
        $regex = '/
841
          (
842
            (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
843
            |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
844
            |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
845
            |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3
846
            ){1,100}                      # ...one or more times
847
          )
848
        | ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
849
        | ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
850
        /x';
851
        /** @noinspection NotOptimalRegularExpressionsInspection */
852 87
        $str = (string) \preg_replace($regex, '$1', $str);
853
854 87
        if ($replace_diamond_question_mark === true) {
855 33
            $str = self::replace_diamond_question_mark($str, '');
856
        }
857
858 87
        if ($remove_invisible_characters === true) {
859 87
            $str = self::remove_invisible_characters($str, $remove_invisible_characters_url_encoded);
860
        }
861
862 87
        if ($normalize_whitespace === true) {
863 37
            $str = self::normalize_whitespace($str, $keep_non_breaking_space);
864
        }
865
866 87
        if ($normalize_msword === true) {
867 4
            $str = self::normalize_msword($str);
868
        }
869
870 87
        if ($remove_bom === true) {
871 37
            $str = self::remove_bom($str);
872
        }
873
874 87
        return $str;
875
    }
876
877
    /**
878
     * Clean-up a string and show only printable UTF-8 chars at the end  + fix UTF-8 encoding.
879
     *
880
     * @param string $str <p>The input string.</p>
881
     *
882
     * @psalm-pure
883
     *
884
     * @return string
885
     */
886 33
    public static function cleanup($str): string
887
    {
888
        // init
889 33
        $str = (string) $str;
890
891 33
        if ($str === '') {
892 5
            return '';
893
        }
894
895
        // fixed ISO <-> UTF-8 Errors
896 33
        $str = self::fix_simple_utf8($str);
897
898
        // remove all none UTF-8 symbols
899
        // && remove diamond question mark (�)
900
        // && remove remove invisible characters (e.g. "\0")
901
        // && remove BOM
902
        // && normalize whitespace chars (but keep non-breaking-spaces)
903 33
        return self::clean(
904 33
            $str,
905 33
            true,
906 33
            true,
907 33
            false,
908 33
            true,
909 33
            true,
910 33
            true
911
        );
912
    }
913
914
    /**
915
     * Accepts a string or a array of strings and returns an array of Unicode code points.
916
     *
917
     * INFO: opposite to UTF8::string()
918
     *
919
     * @param string|string[] $arg     <p>A UTF-8 encoded string or an array of such strings.</p>
920
     * @param bool            $u_style <p>If True, will return code points in U+xxxx format,
921
     *                                 default, code points will be returned as integers.</p>
922
     *
923
     * @psalm-pure
924
     *
925
     * @return array<int|string>
926
     *                           The array of code points:<br>
927
     *                           array<int> for $u_style === false<br>
928
     *                           array<string> for $u_style === true<br>
929
     */
930 12
    public static function codepoints($arg, bool $u_style = false): array
931
    {
932 12
        if (\is_string($arg) === true) {
933 12
            $arg = self::str_split($arg);
934
        }
935
936
        /**
937
         * @psalm-suppress DocblockTypeContradiction
938
         */
939 12
        if (!\is_array($arg)) {
0 ignored issues
show
introduced by
The condition is_array($arg) is always true.
Loading history...
940 4
            return [];
941
        }
942
943 12
        if ($arg === []) {
944 7
            return [];
945
        }
946
947 11
        $arg = \array_map(
948
            [
949 11
                self::class,
950
                'ord',
951
            ],
952 11
            $arg
953
        );
954
955 11
        if ($u_style === true) {
956 2
            $arg = \array_map(
957
                [
958 2
                    self::class,
959
                    'int_to_hex',
960
                ],
961 2
                $arg
962
            );
963
        }
964
965 11
        return $arg;
966
    }
967
968
    /**
969
     * Trims the string and replaces consecutive whitespace characters with a
970
     * single space. This includes tabs and newline characters, as well as
971
     * multibyte whitespace such as the thin space and ideographic space.
972
     *
973
     * @param string $str <p>The input string.</p>
974
     *
975
     * @psalm-pure
976
     *
977
     * @return string string with a trimmed $str and condensed whitespace
978
     */
979 13
    public static function collapse_whitespace(string $str): string
980
    {
981 13
        if (self::$SUPPORT['mbstring'] === true) {
982
            /** @noinspection PhpComposerExtensionStubsInspection */
983 13
            return \trim((string) \mb_ereg_replace('[[:space:]]+', ' ', $str));
984
        }
985
986
        return \trim(self::regex_replace($str, '[[:space:]]+', ' '));
987
    }
988
989
    /**
990
     * Returns count of characters used in a string.
991
     *
992
     * @param string $str                     <p>The input string.</p>
993
     * @param bool   $clean_utf8              [optional] <p>Remove non UTF-8 chars from the string.</p>
994
     * @param bool   $try_to_use_mb_functions [optional] <p>Set to false, if you don't want to use
995
     *
996
     * @psalm-pure
997
     *
998
     * @return int[] an associative array of Character as keys and
999
     *               their count as values
1000
     */
1001 19
    public static function count_chars(
1002
        string $str,
1003
        bool $clean_utf8 = false,
1004
        bool $try_to_use_mb_functions = true
1005
    ): array {
1006 19
        return \array_count_values(
1007 19
            self::str_split(
1008 19
                $str,
1009 19
                1,
1010 19
                $clean_utf8,
1011 19
                $try_to_use_mb_functions
1012
            )
1013
        );
1014
    }
1015
1016
    /**
1017
     * Remove css media-queries.
1018
     *
1019
     * @param string $str
1020
     *
1021
     * @psalm-pure
1022
     *
1023
     * @return string
1024
     */
1025 1
    public static function css_stripe_media_queries(string $str): string
1026
    {
1027 1
        return (string) \preg_replace(
1028 1
            '#@media\\s+(?:only\\s)?(?:[\\s{(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#isumU',
1029 1
            '',
1030 1
            $str
1031
        );
1032
    }
1033
1034
    /**
1035
     * Checks whether ctype is available on the server.
1036
     *
1037
     * @psalm-pure
1038
     *
1039
     * @return bool
1040
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
1041
     */
1042
    public static function ctype_loaded(): bool
1043
    {
1044
        return \extension_loaded('ctype');
1045
    }
1046
1047
    /**
1048
     * Converts an int value into a UTF-8 character.
1049
     *
1050
     * @param mixed $int
1051
     *
1052
     * @psalm-pure
1053
     *
1054
     * @return string
1055
     */
1056 20
    public static function decimal_to_chr($int): string
1057
    {
1058 20
        return self::html_entity_decode('&#' . $int . ';', \ENT_QUOTES | \ENT_HTML5);
1059
    }
1060
1061
    /**
1062
     * Decodes a MIME header field
1063
     *
1064
     * @param string $str
1065
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
1066
     *
1067
     * @psalm-pure
1068
     *
1069
     * @return false|string
1070
     *                      A decoded MIME field on success,
1071
     *                      or false if an error occurs during the decoding
1072
     */
1073
    public static function decode_mimeheader($str, string $encoding = 'UTF-8')
1074
    {
1075
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1076
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1077
        }
1078
1079
        if (self::$SUPPORT['iconv'] === true) {
1080
            return \iconv_mime_decode($str, \ICONV_MIME_DECODE_CONTINUE_ON_ERROR, $encoding);
1081
        }
1082
1083
        if ($encoding !== 'UTF-8') {
1084
            $str = self::encode($encoding, $str);
1085
        }
1086
1087
        return \mb_decode_mimeheader($str);
1088
    }
1089
1090
    /**
1091
     * Decodes a string which was encoded by "UTF8::emoji_encode()".
1092
     *
1093
     * @param string $str                            <p>The input string.</p>
1094
     * @param bool   $use_reversible_string_mappings [optional] <p>
1095
     *                                               When <b>TRUE</b>, we se a reversible string mapping
1096
     *                                               between "emoji_encode" and "emoji_decode".</p>
1097
     *
1098
     * @psalm-pure
1099
     *
1100
     * @return string
1101
     */
1102 9
    public static function emoji_decode(
1103
        string $str,
1104
        bool $use_reversible_string_mappings = false
1105
    ): string {
1106 9
        self::initEmojiData();
1107
1108 9
        if ($use_reversible_string_mappings === true) {
1109 9
            return (string) \str_replace(
1110 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1111 9
                (array) self::$EMOJI_VALUES_CACHE,
1112 9
                $str
1113
            );
1114
        }
1115
1116 1
        return (string) \str_replace(
1117 1
            (array) self::$EMOJI_KEYS_CACHE,
1118 1
            (array) self::$EMOJI_VALUES_CACHE,
1119 1
            $str
1120
        );
1121
    }
1122
1123
    /**
1124
     * Encode a string with emoji chars into a non-emoji string.
1125
     *
1126
     * @param string $str                            <p>The input string</p>
1127
     * @param bool   $use_reversible_string_mappings [optional] <p>
1128
     *                                               when <b>TRUE</b>, we se a reversible string mapping
1129
     *                                               between "emoji_encode" and "emoji_decode"</p>
1130
     *
1131
     * @psalm-pure
1132
     *
1133
     * @return string
1134
     */
1135 12
    public static function emoji_encode(
1136
        string $str,
1137
        bool $use_reversible_string_mappings = false
1138
    ): string {
1139 12
        self::initEmojiData();
1140
1141 12
        if ($use_reversible_string_mappings === true) {
1142 9
            return (string) \str_replace(
1143 9
                (array) self::$EMOJI_VALUES_CACHE,
1144 9
                (array) self::$EMOJI_KEYS_REVERSIBLE_CACHE,
1145 9
                $str
1146
            );
1147
        }
1148
1149 4
        return (string) \str_replace(
1150 4
            (array) self::$EMOJI_VALUES_CACHE,
1151 4
            (array) self::$EMOJI_KEYS_CACHE,
1152 4
            $str
1153
        );
1154
    }
1155
1156
    /**
1157
     * Encode a string with a new charset-encoding.
1158
     *
1159
     * INFO:  This function will also try to fix broken / double encoding,
1160
     *        so you can call this function also on a UTF-8 string and you don't mess up the string.
1161
     *
1162
     * @param string $to_encoding                   <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.</p>
1163
     * @param string $str                           <p>The input string</p>
1164
     * @param bool   $auto_detect_the_from_encoding [optional] <p>Force the new encoding (we try to fix broken / double
1165
     *                                              encoding for UTF-8)<br> otherwise we auto-detect the current
1166
     *                                              string-encoding</p>
1167
     * @param string $from_encoding                 [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1168
     *                                              A empty string will trigger the autodetect anyway.</p>
1169
     *
1170
     * @psalm-pure
1171
     *
1172
     * @return string
1173
     *
1174
     * @psalm-suppress InvalidReturnStatement
1175
     */
1176 28
    public static function encode(
1177
        string $to_encoding,
1178
        string $str,
1179
        bool $auto_detect_the_from_encoding = true,
1180
        string $from_encoding = ''
1181
    ): string {
1182 28
        if ($str === '' || $to_encoding === '') {
1183 13
            return $str;
1184
        }
1185
1186 28
        if ($to_encoding !== 'UTF-8' && $to_encoding !== 'CP850') {
1187 7
            $to_encoding = self::normalize_encoding($to_encoding, 'UTF-8');
1188
        }
1189
1190 28
        if ($from_encoding && $from_encoding !== 'UTF-8' && $from_encoding !== 'CP850') {
1191 2
            $from_encoding = self::normalize_encoding($from_encoding, null);
1192
        }
1193
1194
        if (
1195 28
            $to_encoding
1196
            &&
1197 28
            $from_encoding
1198
            &&
1199 28
            $from_encoding === $to_encoding
1200
        ) {
1201
            return $str;
1202
        }
1203
1204 28
        if ($to_encoding === 'JSON') {
1205 1
            $return = self::json_encode($str);
1206 1
            if ($return === false) {
1207
                throw new \InvalidArgumentException('The input string [' . $str . '] can not be used for json_encode().');
1208
            }
1209
1210 1
            return $return;
1211
        }
1212 28
        if ($from_encoding === 'JSON') {
1213 1
            $str = self::json_decode($str);
1214 1
            $from_encoding = '';
1215
        }
1216
1217 28
        if ($to_encoding === 'BASE64') {
1218 2
            return \base64_encode($str);
1219
        }
1220 28
        if ($from_encoding === 'BASE64') {
1221 2
            $str = \base64_decode($str, true);
1222 2
            $from_encoding = '';
1223
        }
1224
1225 28
        if ($to_encoding === 'HTML-ENTITIES') {
1226 2
            return self::html_encode($str, true, 'UTF-8');
1227
        }
1228 28
        if ($from_encoding === 'HTML-ENTITIES') {
1229 2
            $str = self::html_entity_decode($str, \ENT_COMPAT, 'UTF-8');
1230 2
            $from_encoding = '';
1231
        }
1232
1233 28
        $from_encoding_auto_detected = false;
1234
        if (
1235 28
            $auto_detect_the_from_encoding === true
1236
            ||
1237 28
            !$from_encoding
1238
        ) {
1239 28
            $from_encoding_auto_detected = self::str_detect_encoding($str);
1240
        }
1241
1242
        // DEBUG
1243
        //var_dump($to_encoding, $from_encoding, $from_encoding_auto_detected, $str, "\n\n");
1244
1245 28
        if ($from_encoding_auto_detected !== false) {
1246
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1247 24
            $from_encoding = $from_encoding_auto_detected;
1248 7
        } elseif ($auto_detect_the_from_encoding === true) {
1249
            // fallback for the "autodetect"-mode
1250 7
            return self::to_utf8($str);
1251
        }
1252
1253
        if (
1254 24
            !$from_encoding
1255
            ||
1256 24
            $from_encoding === $to_encoding
1257
        ) {
1258 15
            return $str;
1259
        }
1260
1261
        if (
1262 19
            $to_encoding === 'UTF-8'
1263
            &&
1264
            (
1265 17
                $from_encoding === 'WINDOWS-1252'
1266
                ||
1267 19
                $from_encoding === 'ISO-8859-1'
1268
            )
1269
        ) {
1270 13
            return self::to_utf8($str);
1271
        }
1272
1273
        if (
1274 12
            $to_encoding === 'ISO-8859-1'
1275
            &&
1276
            (
1277 6
                $from_encoding === 'WINDOWS-1252'
1278
                ||
1279 12
                $from_encoding === 'UTF-8'
1280
            )
1281
        ) {
1282 6
            return self::to_iso8859($str);
1283
        }
1284
1285
        if (
1286 10
            $to_encoding !== 'UTF-8'
1287
            &&
1288 10
            $to_encoding !== 'ISO-8859-1'
1289
            &&
1290 10
            $to_encoding !== 'WINDOWS-1252'
1291
            &&
1292 10
            self::$SUPPORT['mbstring'] === false
1293
        ) {
1294
            /**
1295
             * @psalm-suppress ImpureFunctionCall - is is only a warning
1296
             */
1297
            \trigger_error('UTF8::encode() without mbstring cannot handle "' . $to_encoding . '" encoding', \E_USER_WARNING);
1298
        }
1299
1300 10
        if (self::$SUPPORT['mbstring'] === true) {
1301
            // warning: do not use the symfony polyfill here
1302 10
            $str_encoded = \mb_convert_encoding(
1303 10
                $str,
1304 10
                $to_encoding,
1305 10
                $from_encoding
1306
            );
1307
1308 10
            if ($str_encoded) {
1309 10
                return $str_encoded;
1310
            }
1311
        }
1312
1313
        $return = \iconv($from_encoding, $to_encoding, $str);
1314
        if ($return !== false) {
1315
            return $return;
1316
        }
1317
1318
        return $str;
1319
    }
1320
1321
    /**
1322
     * @param string $str
1323
     * @param string $from_charset      [optional] <p>Set the input charset.</p>
1324
     * @param string $to_charset        [optional] <p>Set the output charset.</p>
1325
     * @param string $transfer_encoding [optional] <p>Set the transfer encoding.</p>
1326
     * @param string $linefeed          [optional] <p>Set the used linefeed.</p>
1327
     * @param int    $indent            [optional] <p>Set the max length indent.</p>
1328
     *
1329
     * @psalm-pure
1330
     *
1331
     * @return false|string
1332
     *                      <p>An encoded MIME field on success,
1333
     *                      or false if an error occurs during the encoding.</p>
1334
     */
1335
    public static function encode_mimeheader(
1336
        $str,
1337
        $from_charset = 'UTF-8',
1338
        $to_charset = 'UTF-8',
1339
        $transfer_encoding = 'Q',
1340
        $linefeed = '\\r\\n',
1341
        $indent = 76
1342
    ) {
1343
        if ($from_charset !== 'UTF-8' && $from_charset !== 'CP850') {
1344
            $from_charset = self::normalize_encoding($from_charset, 'UTF-8');
1345
        }
1346
1347
        if ($to_charset !== 'UTF-8' && $to_charset !== 'CP850') {
1348
            $to_charset = self::normalize_encoding($to_charset, 'UTF-8');
1349
        }
1350
1351
        return \iconv_mime_encode(
1352
            '',
1353
            $str,
1354
            [
1355
                'scheme'           => $transfer_encoding,
1356
                'line-length'      => $indent,
1357
                'input-charset'    => $from_charset,
1358
                'output-charset'   => $to_charset,
1359
                'line-break-chars' => $linefeed,
1360
            ]
1361
        );
1362
    }
1363
1364
    /**
1365
     * Create an extract from a sentence, so if the search-string was found, it try to centered in the output.
1366
     *
1367
     * @param string   $str                       <p>The input string.</p>
1368
     * @param string   $search                    <p>The searched string.</p>
1369
     * @param int|null $length                    [optional] <p>Default: null === text->length / 2</p>
1370
     * @param string   $replacer_for_skipped_text [optional] <p>Default: …</p>
1371
     * @param string   $encoding                  [optional] <p>Set the charset for e.g. "mb_" function</p>
1372
     *
1373
     * @psalm-pure
1374
     *
1375
     * @return string
1376
     */
1377 1
    public static function extract_text(
1378
        string $str,
1379
        string $search = '',
1380
        int $length = null,
1381
        string $replacer_for_skipped_text = '…',
1382
        string $encoding = 'UTF-8'
1383
    ): string {
1384 1
        if ($str === '') {
1385 1
            return '';
1386
        }
1387
1388 1
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
1389
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
1390
        }
1391
1392 1
        $trim_chars = "\t\r\n -_()!~?=+/*\\,.:;\"'[]{}`&";
1393
1394 1
        if ($length === null) {
1395 1
            $length = (int) \round((int) self::strlen($str, $encoding) / 2, 0);
1396
        }
1397
1398 1
        if ($search === '') {
1399 1
            if ($encoding === 'UTF-8') {
1400 1
                if ($length > 0) {
1401 1
                    $string_length = (int) \mb_strlen($str);
1402 1
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1403
                } else {
1404 1
                    $end = 0;
1405
                }
1406
1407 1
                $pos = (int) \min(
1408 1
                    \mb_strpos($str, ' ', $end),
1409 1
                    \mb_strpos($str, '.', $end)
1410
                );
1411
            } else {
1412
                if ($length > 0) {
1413
                    $string_length = (int) self::strlen($str, $encoding);
1414
                    $end = ($length - 1) > $string_length ? $string_length : ($length - 1);
1415
                } else {
1416
                    $end = 0;
1417
                }
1418
1419
                $pos = (int) \min(
1420
                    self::strpos($str, ' ', $end, $encoding),
1421
                    self::strpos($str, '.', $end, $encoding)
1422
                );
1423
            }
1424
1425 1
            if ($pos) {
1426 1
                if ($encoding === 'UTF-8') {
1427 1
                    $str_sub = \mb_substr($str, 0, $pos);
1428
                } else {
1429
                    $str_sub = self::substr($str, 0, $pos, $encoding);
1430
                }
1431
1432 1
                if ($str_sub === false) {
1433
                    return '';
1434
                }
1435
1436 1
                return \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1437
            }
1438
1439
            return $str;
1440
        }
1441
1442 1
        if ($encoding === 'UTF-8') {
1443 1
            $word_position = (int) \mb_stripos($str, $search);
1444 1
            $half_side = (int) ($word_position - $length / 2 + (int) \mb_strlen($search) / 2);
1445
        } else {
1446
            $word_position = (int) self::stripos($str, $search, 0, $encoding);
1447
            $half_side = (int) ($word_position - $length / 2 + (int) self::strlen($search, $encoding) / 2);
1448
        }
1449
1450 1
        $pos_start = 0;
1451 1
        if ($half_side > 0) {
1452 1
            if ($encoding === 'UTF-8') {
1453 1
                $half_text = \mb_substr($str, 0, $half_side);
1454
            } else {
1455
                $half_text = self::substr($str, 0, $half_side, $encoding);
1456
            }
1457 1
            if ($half_text !== false) {
1458 1
                if ($encoding === 'UTF-8') {
1459 1
                    $pos_start = (int) \max(
1460 1
                        \mb_strrpos($half_text, ' '),
1461 1
                        \mb_strrpos($half_text, '.')
1462
                    );
1463
                } else {
1464
                    $pos_start = (int) \max(
1465
                        self::strrpos($half_text, ' ', 0, $encoding),
1466
                        self::strrpos($half_text, '.', 0, $encoding)
1467
                    );
1468
                }
1469
            }
1470
        }
1471
1472 1
        if ($word_position && $half_side > 0) {
1473 1
            $offset = $pos_start + $length - 1;
1474 1
            $real_length = (int) self::strlen($str, $encoding);
1475
1476 1
            if ($offset > $real_length) {
1477
                $offset = $real_length;
1478
            }
1479
1480 1
            if ($encoding === 'UTF-8') {
1481 1
                $pos_end = (int) \min(
1482 1
                    \mb_strpos($str, ' ', $offset),
1483 1
                    \mb_strpos($str, '.', $offset)
1484 1
                ) - $pos_start;
1485
            } else {
1486
                $pos_end = (int) \min(
1487
                    self::strpos($str, ' ', $offset, $encoding),
1488
                    self::strpos($str, '.', $offset, $encoding)
1489
                ) - $pos_start;
1490
            }
1491
1492 1
            if (!$pos_end || $pos_end <= 0) {
1493 1
                if ($encoding === 'UTF-8') {
1494 1
                    $str_sub = \mb_substr($str, $pos_start, (int) \mb_strlen($str));
1495
                } else {
1496
                    $str_sub = self::substr($str, $pos_start, (int) self::strlen($str, $encoding), $encoding);
1497
                }
1498 1
                if ($str_sub !== false) {
1499 1
                    $extract = $replacer_for_skipped_text . \ltrim($str_sub, $trim_chars);
1500
                } else {
1501 1
                    $extract = '';
1502
                }
1503
            } else {
1504 1
                if ($encoding === 'UTF-8') {
1505 1
                    $str_sub = \mb_substr($str, $pos_start, $pos_end);
1506
                } else {
1507
                    $str_sub = self::substr($str, $pos_start, $pos_end, $encoding);
1508
                }
1509 1
                if ($str_sub !== false) {
1510 1
                    $extract = $replacer_for_skipped_text . \trim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1511
                } else {
1512 1
                    $extract = '';
1513
                }
1514
            }
1515
        } else {
1516 1
            $offset = $length - 1;
1517 1
            $true_length = (int) self::strlen($str, $encoding);
1518
1519 1
            if ($offset > $true_length) {
1520
                $offset = $true_length;
1521
            }
1522
1523 1
            if ($encoding === 'UTF-8') {
1524 1
                $pos_end = (int) \min(
1525 1
                    \mb_strpos($str, ' ', $offset),
1526 1
                    \mb_strpos($str, '.', $offset)
1527
                );
1528
            } else {
1529
                $pos_end = (int) \min(
1530
                    self::strpos($str, ' ', $offset, $encoding),
1531
                    self::strpos($str, '.', $offset, $encoding)
1532
                );
1533
            }
1534
1535 1
            if ($pos_end) {
1536 1
                if ($encoding === 'UTF-8') {
1537 1
                    $str_sub = \mb_substr($str, 0, $pos_end);
1538
                } else {
1539
                    $str_sub = self::substr($str, 0, $pos_end, $encoding);
1540
                }
1541 1
                if ($str_sub !== false) {
1542 1
                    $extract = \rtrim($str_sub, $trim_chars) . $replacer_for_skipped_text;
1543
                } else {
1544 1
                    $extract = '';
1545
                }
1546
            } else {
1547 1
                $extract = $str;
1548
            }
1549
        }
1550
1551 1
        return $extract;
1552
    }
1553
1554
    /**
1555
     * Reads entire file into a string.
1556
     *
1557
     * WARNING: Do not use UTF-8 Option ($convert_to_utf8) for binary files (e.g.: images) !!!
1558
     *
1559
     * @see http://php.net/manual/en/function.file-get-contents.php
1560
     *
1561
     * @param string        $filename         <p>
1562
     *                                        Name of the file to read.
1563
     *                                        </p>
1564
     * @param bool          $use_include_path [optional] <p>
1565
     *                                        Prior to PHP 5, this parameter is called
1566
     *                                        use_include_path and is a bool.
1567
     *                                        As of PHP 5 the FILE_USE_INCLUDE_PATH can be used
1568
     *                                        to trigger include path
1569
     *                                        search.
1570
     *                                        </p>
1571
     * @param resource|null $context          [optional] <p>
1572
     *                                        A valid context resource created with
1573
     *                                        stream_context_create. If you don't need to use a
1574
     *                                        custom context, you can skip this parameter by &null;.
1575
     *                                        </p>
1576
     * @param int|null      $offset           [optional] <p>
1577
     *                                        The offset where the reading starts.
1578
     *                                        </p>
1579
     * @param int|null      $max_length       [optional] <p>
1580
     *                                        Maximum length of data read. The default is to read until end
1581
     *                                        of file is reached.
1582
     *                                        </p>
1583
     * @param int           $timeout          <p>The time in seconds for the timeout.</p>
1584
     * @param bool          $convert_to_utf8  <strong>WARNING!!!</strong> <p>Maybe you can't use this option for
1585
     *                                        some files, because they used non default utf-8 chars. Binary files
1586
     *                                        like images or pdf will not be converted.</p>
1587
     * @param string        $from_encoding    [optional] <p>e.g. 'UTF-16', 'UTF-8', 'ISO-8859-1', etc.<br>
1588
     *                                        A empty string will trigger the autodetect anyway.</p>
1589
     *
1590
     * @psalm-pure
1591
     *
1592
     * @return false|string
1593
     *                      <p>The function returns the read data as string or <b>false</b> on failure.</p>
1594
     */
1595 12
    public static function file_get_contents(
1596
        string $filename,
1597
        bool $use_include_path = false,
1598
        $context = null,
1599
        int $offset = null,
1600
        int $max_length = null,
1601
        int $timeout = 10,
1602
        bool $convert_to_utf8 = true,
1603
        string $from_encoding = ''
1604
    ) {
1605
        // init
1606 12
        $filename = \filter_var($filename, \FILTER_SANITIZE_STRING);
1607
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
1608 12
        if ($filename === false) {
1609
            return false;
1610
        }
1611
1612 12
        if ($timeout && $context === null) {
1613 9
            $context = \stream_context_create(
1614
                [
1615
                    'http' => [
1616 9
                        'timeout' => $timeout,
1617
                    ],
1618
                ]
1619
            );
1620
        }
1621
1622 12
        if ($offset === null) {
1623 12
            $offset = 0;
1624
        }
1625
1626 12
        if (\is_int($max_length) === true) {
1627 2
            $data = \file_get_contents($filename, $use_include_path, $context, $offset, $max_length);
1628
        } else {
1629 12
            $data = \file_get_contents($filename, $use_include_path, $context, $offset);
1630
        }
1631
1632
        // return false on error
1633 12
        if ($data === false) {
1634
            return false;
1635
        }
1636
1637 12
        if ($convert_to_utf8 === true) {
1638
            if (
1639 12
                self::is_binary($data, true) !== true
1640
                ||
1641 9
                self::is_utf16($data, false) !== false
1642
                ||
1643 12
                self::is_utf32($data, false) !== false
1644
            ) {
1645 9
                $data = self::encode('UTF-8', $data, false, $from_encoding);
1646 9
                $data = self::cleanup($data);
1647
            }
1648
        }
1649
1650 12
        return $data;
1651
    }
1652
1653
    /**
1654
     * Checks if a file starts with BOM (Byte Order Mark) character.
1655
     *
1656
     * @param string $file_path <p>Path to a valid file.</p>
1657
     *
1658
     * @throws \RuntimeException if file_get_contents() returned false
1659
     *
1660
     * @return bool
1661
     *              <p><strong>true</strong> if the file has BOM at the start, <strong>false</strong> otherwise</p>
1662
     *
1663
     * @psalm-pure
1664
     */
1665 2
    public static function file_has_bom(string $file_path): bool
1666
    {
1667 2
        $file_content = \file_get_contents($file_path);
1668 2
        if ($file_content === false) {
1669
            throw new \RuntimeException('file_get_contents() returned false for:' . $file_path);
1670
        }
1671
1672 2
        return self::string_has_bom($file_content);
1673
    }
1674
1675
    /**
1676
     * Normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1677
     *
1678
     * @param mixed  $var
1679
     * @param int    $normalization_form
1680
     * @param string $leading_combining
1681
     *
1682
     * @psalm-pure
1683
     *
1684
     * @return mixed
1685
     */
1686 62
    public static function filter(
1687
        $var,
1688
        int $normalization_form = \Normalizer::NFC,
1689
        string $leading_combining = '◌'
1690
    ) {
1691 62
        switch (\gettype($var)) {
1692 62
            case 'array':
1693
                /** @noinspection ForeachSourceInspection */
1694 6
                foreach ($var as $k => &$v) {
1695 6
                    $v = self::filter($v, $normalization_form, $leading_combining);
1696
                }
1697 6
                unset($v);
1698
1699 6
                break;
1700 62
            case 'object':
1701
                /** @noinspection ForeachSourceInspection */
1702 4
                foreach ($var as $k => &$v) {
1703 4
                    $v = self::filter($v, $normalization_form, $leading_combining);
1704
                }
1705 4
                unset($v);
1706
1707 4
                break;
1708 62
            case 'string':
1709
1710 62
                if (\strpos($var, "\r") !== false) {
1711
                    // Workaround https://bugs.php.net/65732
1712 3
                    $var = self::normalize_line_ending($var);
1713
                }
1714
1715 62
                if (ASCII::is_ascii($var) === false) {
1716 32
                    if (\Normalizer::isNormalized($var, $normalization_form)) {
1717 27
                        $n = '-';
1718
                    } else {
1719 12
                        $n = \Normalizer::normalize($var, $normalization_form);
1720
1721 12
                        if (isset($n[0])) {
1722 7
                            $var = $n;
1723
                        } else {
1724 8
                            $var = self::encode('UTF-8', $var, true);
1725
                        }
1726
                    }
1727
1728
                    if (
1729 32
                        $var[0] >= "\x80"
1730
                        &&
1731 32
                        isset($n[0], $leading_combining[0])
1732
                        &&
1733 32
                        \preg_match('/^\\p{Mn}/u', $var)
1734
                    ) {
1735
                        // Prevent leading combining chars
1736
                        // for NFC-safe concatenations.
1737 3
                        $var = $leading_combining . $var;
1738
                    }
1739
                }
1740
1741 62
                break;
1742
        }
1743
1744 62
        return $var;
1745
    }
1746
1747
    /**
1748
     * "filter_input()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1749
     *
1750
     * Gets a specific external variable by name and optionally filters it
1751
     *
1752
     * @see http://php.net/manual/en/function.filter-input.php
1753
     *
1754
     * @param int    $type          <p>
1755
     *                              One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1756
     *                              <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1757
     *                              <b>INPUT_ENV</b>.
1758
     *                              </p>
1759
     * @param string $variable_name <p>
1760
     *                              Name of a variable to get.
1761
     *                              </p>
1762
     * @param int    $filter        [optional] <p>
1763
     *                              The ID of the filter to apply. The
1764
     *                              manual page lists the available filters.
1765
     *                              </p>
1766
     * @param mixed  $options       [optional] <p>
1767
     *                              Associative array of options or bitwise disjunction of flags. If filter
1768
     *                              accepts options, flags can be provided in "flags" field of array.
1769
     *                              </p>
1770
     *
1771
     * @psalm-pure
1772
     *
1773
     * @return mixed Value of the requested variable on success, <b>FALSE</b> if the filter fails, or <b>NULL</b> if the
1774
     *               <i>variable_name</i> variable is not set. If the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it
1775
     *               returns <b>FALSE</b> if the variable is not set and <b>NULL</b> if the filter fails.
1776
     */
1777
    public static function filter_input(
1778
        int $type,
1779
        string $variable_name,
1780
        int $filter = \FILTER_DEFAULT,
1781
        $options = null
1782
    ) {
1783
        /**
1784
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1785
         */
1786
        if ($options === null || \func_num_args() < 4) {
1787
            $var = \filter_input($type, $variable_name, $filter);
1788
        } else {
1789
            $var = \filter_input($type, $variable_name, $filter, $options);
1790
        }
1791
1792
        return self::filter($var);
1793
    }
1794
1795
    /**
1796
     * "filter_input_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1797
     *
1798
     * Gets external variables and optionally filters them
1799
     *
1800
     * @see http://php.net/manual/en/function.filter-input-array.php
1801
     *
1802
     * @param int   $type       <p>
1803
     *                          One of <b>INPUT_GET</b>, <b>INPUT_POST</b>,
1804
     *                          <b>INPUT_COOKIE</b>, <b>INPUT_SERVER</b>, or
1805
     *                          <b>INPUT_ENV</b>.
1806
     *                          </p>
1807
     * @param mixed $definition [optional] <p>
1808
     *                          An array defining the arguments. A valid key is a string
1809
     *                          containing a variable name and a valid value is either a filter type, or an array
1810
     *                          optionally specifying the filter, flags and options. If the value is an
1811
     *                          array, valid keys are filter which specifies the
1812
     *                          filter type,
1813
     *                          flags which specifies any flags that apply to the
1814
     *                          filter, and options which specifies any options that
1815
     *                          apply to the filter. See the example below for a better understanding.
1816
     *                          </p>
1817
     *                          <p>
1818
     *                          This parameter can be also an integer holding a filter constant. Then all values in the
1819
     *                          input array are filtered by this filter.
1820
     *                          </p>
1821
     * @param bool  $add_empty  [optional] <p>
1822
     *                          Add missing keys as <b>NULL</b> to the return value.
1823
     *                          </p>
1824
     *
1825
     * @psalm-pure
1826
     *
1827
     * @return mixed An array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1828
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1829
     *               set. Or if the flag <b>FILTER_NULL_ON_FAILURE</b> is used, it returns <b>FALSE</b> if the variable
1830
     *               is not set and <b>NULL</b> if the filter fails.
1831
     */
1832
    public static function filter_input_array(
1833
        int $type,
1834
        $definition = null,
1835
        bool $add_empty = true
1836
    ) {
1837
        /**
1838
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1839
         */
1840
        if ($definition === null || \func_num_args() < 2) {
1841
            $a = \filter_input_array($type);
1842
        } else {
1843
            $a = \filter_input_array($type, $definition, $add_empty);
1844
        }
1845
1846
        return self::filter($a);
1847
    }
1848
1849
    /**
1850
     * "filter_var()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1851
     *
1852
     * Filters a variable with a specified filter
1853
     *
1854
     * @see http://php.net/manual/en/function.filter-var.php
1855
     *
1856
     * @param mixed $variable <p>
1857
     *                        Value to filter.
1858
     *                        </p>
1859
     * @param int   $filter   [optional] <p>
1860
     *                        The ID of the filter to apply. The
1861
     *                        manual page lists the available filters.
1862
     *                        </p>
1863
     * @param mixed $options  [optional] <p>
1864
     *                        Associative array of options or bitwise disjunction of flags. If filter
1865
     *                        accepts options, flags can be provided in "flags" field of array. For
1866
     *                        the "callback" filter, callable type should be passed. The
1867
     *                        callback must accept one argument, the value to be filtered, and return
1868
     *                        the value after filtering/sanitizing it.
1869
     *                        </p>
1870
     *                        <p>
1871
     *                        <code>
1872
     *                        // for filters that accept options, use this format
1873
     *                        $options = array(
1874
     *                        'options' => array(
1875
     *                        'default' => 3, // value to return if the filter fails
1876
     *                        // other options here
1877
     *                        'min_range' => 0
1878
     *                        ),
1879
     *                        'flags' => FILTER_FLAG_ALLOW_OCTAL,
1880
     *                        );
1881
     *                        $var = filter_var('0755', FILTER_VALIDATE_INT, $options);
1882
     *                        // for filter that only accept flags, you can pass them directly
1883
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
1884
     *                        // for filter that only accept flags, you can also pass as an array
1885
     *                        $var = filter_var('oops', FILTER_VALIDATE_BOOLEAN,
1886
     *                        array('flags' => FILTER_NULL_ON_FAILURE));
1887
     *                        // callback validate filter
1888
     *                        function foo($value)
1889
     *                        {
1890
     *                        // Expected format: Surname, GivenNames
1891
     *                        if (strpos($value, ", ") === false) return false;
1892
     *                        list($surname, $givennames) = explode(", ", $value, 2);
1893
     *                        $empty = (empty($surname) || empty($givennames));
1894
     *                        $notstrings = (!is_string($surname) || !is_string($givennames));
1895
     *                        if ($empty || $notstrings) {
1896
     *                        return false;
1897
     *                        } else {
1898
     *                        return $value;
1899
     *                        }
1900
     *                        }
1901
     *                        $var = filter_var('Doe, Jane Sue', FILTER_CALLBACK, array('options' => 'foo'));
1902
     *                        </code>
1903
     *                        </p>
1904
     *
1905
     * @psalm-pure
1906
     *
1907
     * @return mixed the filtered data, or <b>FALSE</b> if the filter fails
1908
     */
1909 2
    public static function filter_var(
1910
        $variable,
1911
        int $filter = \FILTER_DEFAULT,
1912
        $options = null
1913
    ) {
1914
        /**
1915
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1916
         */
1917 2
        if (\func_num_args() < 3) {
1918 2
            $variable = \filter_var($variable, $filter);
1919
        } else {
1920 2
            $variable = \filter_var($variable, $filter, $options);
1921
        }
1922
1923 2
        return self::filter($variable);
1924
    }
1925
1926
    /**
1927
     * "filter_var_array()"-wrapper with normalizes to UTF-8 NFC, converting from WINDOWS-1252 when needed.
1928
     *
1929
     * Gets multiple variables and optionally filters them
1930
     *
1931
     * @see http://php.net/manual/en/function.filter-var-array.php
1932
     *
1933
     * @param array<mixed> $data       <p>
1934
     *                                 An array with string keys containing the data to filter.
1935
     *                                 </p>
1936
     * @param mixed        $definition [optional] <p>
1937
     *                                 An array defining the arguments. A valid key is a string
1938
     *                                 containing a variable name and a valid value is either a
1939
     *                                 filter type, or an
1940
     *                                 array optionally specifying the filter, flags and options.
1941
     *                                 If the value is an array, valid keys are filter
1942
     *                                 which specifies the filter type,
1943
     *                                 flags which specifies any flags that apply to the
1944
     *                                 filter, and options which specifies any options that
1945
     *                                 apply to the filter. See the example below for a better understanding.
1946
     *                                 </p>
1947
     *                                 <p>
1948
     *                                 This parameter can be also an integer holding a filter constant. Then all values
1949
     *                                 in the input array are filtered by this filter.
1950
     *                                 </p>
1951
     * @param bool         $add_empty  [optional] <p>
1952
     *                                 Add missing keys as <b>NULL</b> to the return value.
1953
     *                                 </p>
1954
     *
1955
     * @psalm-pure
1956
     *
1957
     * @return mixed an array containing the values of the requested variables on success, or <b>FALSE</b> on failure.
1958
     *               An array value will be <b>FALSE</b> if the filter fails, or <b>NULL</b> if the variable is not
1959
     *               set
1960
     */
1961 2
    public static function filter_var_array(
1962
        array $data,
1963
        $definition = null,
1964
        bool $add_empty = true
1965
    ) {
1966
        /**
1967
         * @psalm-suppress ImpureFunctionCall - we use func_num_args only for args count matching here
1968
         */
1969 2
        if (\func_num_args() < 2) {
1970 2
            $a = \filter_var_array($data);
1971
        } else {
1972 2
            $a = \filter_var_array($data, $definition, $add_empty);
1973
        }
1974
1975 2
        return self::filter($a);
1976
    }
1977
1978
    /**
1979
     * Checks whether finfo is available on the server.
1980
     *
1981
     * @psalm-pure
1982
     *
1983
     * @return bool
1984
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
1985
     */
1986
    public static function finfo_loaded(): bool
1987
    {
1988
        return \class_exists('finfo');
1989
    }
1990
1991
    /**
1992
     * Returns the first $n characters of the string.
1993
     *
1994
     * @param string $str      <p>The input string.</p>
1995
     * @param int    $n        <p>Number of characters to retrieve from the start.</p>
1996
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
1997
     *
1998
     * @psalm-pure
1999
     *
2000
     * @return string
2001
     */
2002 13
    public static function first_char(
2003
        string $str,
2004
        int $n = 1,
2005
        string $encoding = 'UTF-8'
2006
    ): string {
2007 13
        if ($str === '' || $n <= 0) {
2008 5
            return '';
2009
        }
2010
2011 8
        if ($encoding === 'UTF-8') {
2012 4
            return (string) \mb_substr($str, 0, $n);
2013
        }
2014
2015 4
        return (string) self::substr($str, 0, $n, $encoding);
2016
    }
2017
2018
    /**
2019
     * Check if the number of Unicode characters isn't greater than the specified integer.
2020
     *
2021
     * @param string $str      the original string to be checked
2022
     * @param int    $box_size the size in number of chars to be checked against string
2023
     *
2024
     * @psalm-pure
2025
     *
2026
     * @return bool true if string is less than or equal to $box_size, false otherwise
2027
     */
2028 2
    public static function fits_inside(string $str, int $box_size): bool
2029
    {
2030 2
        return (int) self::strlen($str) <= $box_size;
2031
    }
2032
2033
    /**
2034
     * Try to fix simple broken UTF-8 strings.
2035
     *
2036
     * INFO: Take a look at "UTF8::fix_utf8()" if you need a more advanced fix for broken UTF-8 strings.
2037
     *
2038
     * If you received an UTF-8 string that was converted from Windows-1252 as it was ISO-8859-1
2039
     * (ignoring Windows-1252 chars from 80 to 9F) use this function to fix it.
2040
     * See: http://en.wikipedia.org/wiki/Windows-1252
2041
     *
2042
     * @param string $str <p>The input string</p>
2043
     *
2044
     * @psalm-pure
2045
     *
2046
     * @return string
2047
     */
2048 47
    public static function fix_simple_utf8(string $str): string
2049
    {
2050 47
        if ($str === '') {
2051 4
            return '';
2052
        }
2053
2054
        /**
2055
         * @psalm-suppress ImpureStaticVariable
2056
         *
2057
         * @var array<mixed>|null
2058
         */
2059 47
        static $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = null;
2060
2061
        /**
2062
         * @psalm-suppress ImpureStaticVariable
2063
         *
2064
         * @var array<mixed>|null
2065
         */
2066 47
        static $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = null;
2067
2068 47
        if ($BROKEN_UTF8_TO_UTF8_KEYS_CACHE === null) {
2069 1
            if (self::$BROKEN_UTF8_FIX === null) {
2070 1
                self::$BROKEN_UTF8_FIX = self::getData('utf8_fix');
2071
            }
2072
2073 1
            $BROKEN_UTF8_TO_UTF8_KEYS_CACHE = \array_keys(self::$BROKEN_UTF8_FIX);
2074 1
            $BROKEN_UTF8_TO_UTF8_VALUES_CACHE = self::$BROKEN_UTF8_FIX;
2075
        }
2076
2077 47
        \assert(\is_array($BROKEN_UTF8_TO_UTF8_VALUES_CACHE));
2078
2079 47
        return \str_replace($BROKEN_UTF8_TO_UTF8_KEYS_CACHE, $BROKEN_UTF8_TO_UTF8_VALUES_CACHE, $str);
2080
    }
2081
2082
    /**
2083
     * Fix a double (or multiple) encoded UTF8 string.
2084
     *
2085
     * @param string|string[] $str you can use a string or an array of strings
2086
     *
2087
     * @psalm-pure
2088
     *
2089
     * @return string|string[]
2090
     *                         Will return the fixed input-"array" or
2091
     *                         the fixed input-"string"
2092
     *
2093
     * @psalm-suppress InvalidReturnType
2094
     */
2095 2
    public static function fix_utf8($str)
2096
    {
2097 2
        if (\is_array($str) === true) {
2098 2
            foreach ($str as $k => &$v) {
2099 2
                $v = self::fix_utf8($v);
2100
            }
2101 2
            unset($v);
2102
2103
            /**
2104
             * @psalm-suppress InvalidReturnStatement
2105
             */
2106 2
            return $str;
2107
        }
2108
2109 2
        $str = (string) $str;
2110 2
        $last = '';
2111 2
        while ($last !== $str) {
2112 2
            $last = $str;
2113
            /**
2114
             * @psalm-suppress PossiblyInvalidArgument
2115
             */
2116 2
            $str = self::to_utf8(
2117 2
                self::utf8_decode($str, true)
2118
            );
2119
        }
2120
2121
        /**
2122
         * @psalm-suppress InvalidReturnStatement
2123
         */
2124 2
        return $str;
2125
    }
2126
2127
    /**
2128
     * Get character of a specific character.
2129
     *
2130
     * @param string $char
2131
     *
2132
     * @psalm-pure
2133
     *
2134
     * @return string 'RTL' or 'LTR'
2135
     */
2136 2
    public static function getCharDirection(string $char): string
2137
    {
2138 2
        if (self::$SUPPORT['intlChar'] === true) {
2139
            /** @noinspection PhpComposerExtensionStubsInspection */
2140 2
            $tmp_return = \IntlChar::charDirection($char);
2141
2142
            // from "IntlChar"-Class
2143
            $char_direction = [
2144 2
                'RTL' => [1, 13, 14, 15, 21],
2145
                'LTR' => [0, 11, 12, 20],
2146
            ];
2147
2148 2
            if (\in_array($tmp_return, $char_direction['LTR'], true)) {
2149
                return 'LTR';
2150
            }
2151
2152 2
            if (\in_array($tmp_return, $char_direction['RTL'], true)) {
2153 2
                return 'RTL';
2154
            }
2155
        }
2156
2157 2
        $c = static::chr_to_decimal($char);
2158
2159 2
        if (!($c >= 0x5be && $c <= 0x10b7f)) {
2160 2
            return 'LTR';
2161
        }
2162
2163 2
        if ($c <= 0x85e) {
2164 2
            if ($c === 0x5be ||
2165 2
                $c === 0x5c0 ||
2166 2
                $c === 0x5c3 ||
2167 2
                $c === 0x5c6 ||
2168 2
                ($c >= 0x5d0 && $c <= 0x5ea) ||
2169 2
                ($c >= 0x5f0 && $c <= 0x5f4) ||
2170 2
                $c === 0x608 ||
2171 2
                $c === 0x60b ||
2172 2
                $c === 0x60d ||
2173 2
                $c === 0x61b ||
2174 2
                ($c >= 0x61e && $c <= 0x64a) ||
2175
                ($c >= 0x66d && $c <= 0x66f) ||
2176
                ($c >= 0x671 && $c <= 0x6d5) ||
2177
                ($c >= 0x6e5 && $c <= 0x6e6) ||
2178
                ($c >= 0x6ee && $c <= 0x6ef) ||
2179
                ($c >= 0x6fa && $c <= 0x70d) ||
2180
                $c === 0x710 ||
2181
                ($c >= 0x712 && $c <= 0x72f) ||
2182
                ($c >= 0x74d && $c <= 0x7a5) ||
2183
                $c === 0x7b1 ||
2184
                ($c >= 0x7c0 && $c <= 0x7ea) ||
2185
                ($c >= 0x7f4 && $c <= 0x7f5) ||
2186
                $c === 0x7fa ||
2187
                ($c >= 0x800 && $c <= 0x815) ||
2188
                $c === 0x81a ||
2189
                $c === 0x824 ||
2190
                $c === 0x828 ||
2191
                ($c >= 0x830 && $c <= 0x83e) ||
2192
                ($c >= 0x840 && $c <= 0x858) ||
2193 2
                $c === 0x85e
2194
            ) {
2195 2
                return 'RTL';
2196
            }
2197 2
        } elseif ($c === 0x200f) {
2198
            return 'RTL';
2199 2
        } elseif ($c >= 0xfb1d) {
2200 2
            if ($c === 0xfb1d ||
2201 2
                ($c >= 0xfb1f && $c <= 0xfb28) ||
2202 2
                ($c >= 0xfb2a && $c <= 0xfb36) ||
2203 2
                ($c >= 0xfb38 && $c <= 0xfb3c) ||
2204 2
                $c === 0xfb3e ||
2205 2
                ($c >= 0xfb40 && $c <= 0xfb41) ||
2206 2
                ($c >= 0xfb43 && $c <= 0xfb44) ||
2207 2
                ($c >= 0xfb46 && $c <= 0xfbc1) ||
2208 2
                ($c >= 0xfbd3 && $c <= 0xfd3d) ||
2209 2
                ($c >= 0xfd50 && $c <= 0xfd8f) ||
2210 2
                ($c >= 0xfd92 && $c <= 0xfdc7) ||
2211 2
                ($c >= 0xfdf0 && $c <= 0xfdfc) ||
2212 2
                ($c >= 0xfe70 && $c <= 0xfe74) ||
2213 2
                ($c >= 0xfe76 && $c <= 0xfefc) ||
2214 2
                ($c >= 0x10800 && $c <= 0x10805) ||
2215 2
                $c === 0x10808 ||
2216 2
                ($c >= 0x1080a && $c <= 0x10835) ||
2217 2
                ($c >= 0x10837 && $c <= 0x10838) ||
2218 2
                $c === 0x1083c ||
2219 2
                ($c >= 0x1083f && $c <= 0x10855) ||
2220 2
                ($c >= 0x10857 && $c <= 0x1085f) ||
2221 2
                ($c >= 0x10900 && $c <= 0x1091b) ||
2222 2
                ($c >= 0x10920 && $c <= 0x10939) ||
2223 2
                $c === 0x1093f ||
2224 2
                $c === 0x10a00 ||
2225 2
                ($c >= 0x10a10 && $c <= 0x10a13) ||
2226 2
                ($c >= 0x10a15 && $c <= 0x10a17) ||
2227 2
                ($c >= 0x10a19 && $c <= 0x10a33) ||
2228 2
                ($c >= 0x10a40 && $c <= 0x10a47) ||
2229 2
                ($c >= 0x10a50 && $c <= 0x10a58) ||
2230 2
                ($c >= 0x10a60 && $c <= 0x10a7f) ||
2231 2
                ($c >= 0x10b00 && $c <= 0x10b35) ||
2232 2
                ($c >= 0x10b40 && $c <= 0x10b55) ||
2233 2
                ($c >= 0x10b58 && $c <= 0x10b72) ||
2234 2
                ($c >= 0x10b78 && $c <= 0x10b7f)
2235
            ) {
2236 2
                return 'RTL';
2237
            }
2238
        }
2239
2240 2
        return 'LTR';
2241
    }
2242
2243
    /**
2244
     * Check for php-support.
2245
     *
2246
     * @param string|null $key
2247
     *
2248
     * @psalm-pure
2249
     *
2250
     * @return mixed
2251
     *               Return the full support-"array", if $key === null<br>
2252
     *               return bool-value, if $key is used and available<br>
2253
     *               otherwise return <strong>null</strong>
2254
     */
2255 27
    public static function getSupportInfo(string $key = null)
2256
    {
2257 27
        if ($key === null) {
2258 4
            return self::$SUPPORT;
2259
        }
2260
2261 25
        if (self::$INTL_TRANSLITERATOR_LIST === null) {
2262 1
            self::$INTL_TRANSLITERATOR_LIST = self::getData('transliterator_list');
2263
        }
2264
        // compatibility fix for old versions
2265 25
        self::$SUPPORT['intl__transliterator_list_ids'] = self::$INTL_TRANSLITERATOR_LIST;
2266
2267 25
        return self::$SUPPORT[$key] ?? null;
2268
    }
2269
2270
    /**
2271
     * Warning: this method only works for some file-types (png, jpg)
2272
     *          if you need more supported types, please use e.g. "finfo"
2273
     *
2274
     * @param string $str
2275
     * @param array  $fallback <p>with this keys: 'ext', 'mime', 'type'
2276
     *
2277
     * @psalm-pure
2278
     *
2279
     * @return array<string, string|null>
2280
     *                       <p>with this keys: 'ext', 'mime', 'type'</p>
2281
     *
2282
     * @phpstan-param array{ext: null|string, mime: null|string, type: null|string} $fallback
2283
     */
2284 39
    public static function get_file_type(
2285
        string $str,
2286
        array $fallback = [
2287
            'ext'  => null,
2288
            'mime' => 'application/octet-stream',
2289
            'type' => null,
2290
        ]
2291
    ): array {
2292 39
        if ($str === '') {
2293
            return $fallback;
2294
        }
2295
2296
        /** @var false|string $str_info - needed for PhpStan (stubs error) */
2297 39
        $str_info = \substr($str, 0, 2);
2298 39
        if ($str_info === false || \strlen($str_info) !== 2) {
2299 11
            return $fallback;
2300
        }
2301
2302
        // DEBUG
2303
        //var_dump($str_info);
2304
2305 35
        $str_info = \unpack('C2chars', $str_info);
2306
2307
        /** @noinspection PhpSillyAssignmentInspection */
2308
        /** @var array|false $str_info - needed for PhpStan (stubs error) */
2309 35
        $str_info = $str_info;
2310
2311 35
        if ($str_info === false) {
2312
            return $fallback;
2313
        }
2314
        /** @noinspection OffsetOperationsInspection */
2315 35
        $type_code = (int) ($str_info['chars1'] . $str_info['chars2']);
2316
2317
        // DEBUG
2318
        //var_dump($type_code);
2319
2320
        //
2321
        // info: https://en.wikipedia.org/wiki/Magic_number_%28programming%29#Format_indicator
2322
        //
2323
        switch ($type_code) {
2324
            // WARNING: do not add too simple comparisons, because of false-positive results:
2325
            //
2326
            // 3780 => 'pdf', 7790 => 'exe', 7784 => 'midi', 8075 => 'zip',
2327
            // 8297 => 'rar', 7173 => 'gif', 7373 => 'tiff' 6677 => 'bmp', ...
2328
            //
2329 35
            case 255216:
2330
                $ext = 'jpg';
2331
                $mime = 'image/jpeg';
2332
                $type = 'binary';
2333
2334
                break;
2335 35
            case 13780:
2336 7
                $ext = 'png';
2337 7
                $mime = 'image/png';
2338 7
                $type = 'binary';
2339
2340 7
                break;
2341
            default:
2342 34
                return $fallback;
2343
        }
2344
2345
        return [
2346 7
            'ext'  => $ext,
2347 7
            'mime' => $mime,
2348 7
            'type' => $type,
2349
        ];
2350
    }
2351
2352
    /**
2353
     * @param int    $length         <p>Length of the random string.</p>
2354
     * @param string $possible_chars [optional] <p>Characters string for the random selection.</p>
2355
     * @param string $encoding       [optional] <p>Set the charset for e.g. "mb_" function</p>
2356
     *
2357
     * @return string
2358
     */
2359 1
    public static function get_random_string(
2360
        int $length,
2361
        string $possible_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
2362
        string $encoding = 'UTF-8'
2363
    ): string {
2364
        // init
2365 1
        $i = 0;
2366 1
        $str = '';
2367
2368
        //
2369
        // add random chars
2370
        //
2371
2372 1
        if ($encoding === 'UTF-8') {
2373 1
            $max_length = (int) \mb_strlen($possible_chars);
2374 1
            if ($max_length === 0) {
2375 1
                return '';
2376
            }
2377
2378 1
            while ($i < $length) {
2379
                try {
2380 1
                    $rand_int = \random_int(0, $max_length - 1);
2381
                } catch (\Exception $e) {
2382
                    /** @noinspection RandomApiMigrationInspection */
2383
                    $rand_int = \mt_rand(0, $max_length - 1);
2384
                }
2385 1
                $char = \mb_substr($possible_chars, $rand_int, 1);
2386 1
                if ($char !== false) {
2387 1
                    $str .= $char;
2388 1
                    ++$i;
2389
                }
2390
            }
2391
        } else {
2392
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2393
2394
            $max_length = (int) self::strlen($possible_chars, $encoding);
2395
            if ($max_length === 0) {
2396
                return '';
2397
            }
2398
2399
            while ($i < $length) {
2400
                try {
2401
                    $rand_int = \random_int(0, $max_length - 1);
2402
                } catch (\Exception $e) {
2403
                    /** @noinspection RandomApiMigrationInspection */
2404
                    $rand_int = \mt_rand(0, $max_length - 1);
2405
                }
2406
                $char = self::substr($possible_chars, $rand_int, 1, $encoding);
2407
                if ($char !== false) {
2408
                    $str .= $char;
2409
                    ++$i;
2410
                }
2411
            }
2412
        }
2413
2414 1
        return $str;
2415
    }
2416
2417
    /**
2418
     * @param int|string $entropy_extra [optional] <p>Extra entropy via a string or int value.</p>
2419
     * @param bool       $use_md5       [optional] <p>Return the unique identifier as md5-hash? Default: true</p>
2420
     *
2421
     * @return string
2422
     */
2423 1
    public static function get_unique_string($entropy_extra = '', bool $use_md5 = true): string
2424
    {
2425 1
        $unique_helper = \random_int(0, \mt_getrandmax()) .
2426 1
                         \session_id() .
2427 1
                         ($_SERVER['REMOTE_ADDR'] ?? '') .
2428 1
                         ($_SERVER['SERVER_ADDR'] ?? '') .
2429 1
                         $entropy_extra;
2430
2431 1
        $unique_string = \uniqid($unique_helper, true);
2432
2433 1
        if ($use_md5) {
2434 1
            $unique_string = \md5($unique_string . $unique_helper);
2435
        }
2436
2437 1
        return $unique_string;
2438
    }
2439
2440
    /**
2441
     * alias for "UTF8::string_has_bom()"
2442
     *
2443
     * @param string $str
2444
     *
2445
     * @psalm-pure
2446
     *
2447
     * @return bool
2448
     *
2449
     * @see        UTF8::string_has_bom()
2450
     * @deprecated <p>please use "UTF8::string_has_bom()"</p>
2451
     */
2452 2
    public static function hasBom(string $str): bool
2453
    {
2454 2
        return self::string_has_bom($str);
2455
    }
2456
2457
    /**
2458
     * Returns true if the string contains a lower case char, false otherwise.
2459
     *
2460
     * @param string $str <p>The input string.</p>
2461
     *
2462
     * @psalm-pure
2463
     *
2464
     * @return bool
2465
     *              <p>Whether or not the string contains a lower case character.</p>
2466
     */
2467 47
    public static function has_lowercase(string $str): bool
2468
    {
2469 47
        if (self::$SUPPORT['mbstring'] === true) {
2470
            /** @noinspection PhpComposerExtensionStubsInspection */
2471 47
            return \mb_ereg_match('.*[[:lower:]]', $str);
2472
        }
2473
2474
        return self::str_matches_pattern($str, '.*[[:lower:]]');
2475
    }
2476
2477
    /**
2478
     * Returns true if the string contains whitespace, false otherwise.
2479
     *
2480
     * @param string $str <p>The input string.</p>
2481
     *
2482
     * @psalm-pure
2483
     *
2484
     * @return bool
2485
     *              <p>Whether or not the string contains whitespace.</p>
2486
     */
2487 11
    public static function has_whitespace(string $str): bool
2488
    {
2489 11
        if (self::$SUPPORT['mbstring'] === true) {
2490
            /** @noinspection PhpComposerExtensionStubsInspection */
2491 11
            return \mb_ereg_match('.*[[:space:]]', $str);
2492
        }
2493
2494
        return self::str_matches_pattern($str, '.*[[:space:]]');
2495
    }
2496
2497
    /**
2498
     * Returns true if the string contains an upper case char, false otherwise.
2499
     *
2500
     * @param string $str <p>The input string.</p>
2501
     *
2502
     * @psalm-pure
2503
     *
2504
     * @return bool whether or not the string contains an upper case character
2505
     */
2506 12
    public static function has_uppercase(string $str): bool
2507
    {
2508 12
        if (self::$SUPPORT['mbstring'] === true) {
2509
            /** @noinspection PhpComposerExtensionStubsInspection */
2510 12
            return \mb_ereg_match('.*[[:upper:]]', $str);
2511
        }
2512
2513
        return self::str_matches_pattern($str, '.*[[:upper:]]');
2514
    }
2515
2516
    /**
2517
     * Converts a hexadecimal value into a UTF-8 character.
2518
     *
2519
     * @param string $hexdec <p>The hexadecimal value.</p>
2520
     *
2521
     * @psalm-pure
2522
     *
2523
     * @return false|string one single UTF-8 character
2524
     */
2525 4
    public static function hex_to_chr(string $hexdec)
2526
    {
2527 4
        return self::decimal_to_chr(\hexdec($hexdec));
2528
    }
2529
2530
    /**
2531
     * Converts hexadecimal U+xxxx code point representation to integer.
2532
     *
2533
     * INFO: opposite to UTF8::int_to_hex()
2534
     *
2535
     * @param string $hexdec <p>The hexadecimal code point representation.</p>
2536
     *
2537
     * @psalm-pure
2538
     *
2539
     * @return false|int the code point, or false on failure
2540
     */
2541 2
    public static function hex_to_int($hexdec)
2542
    {
2543
        // init
2544 2
        $hexdec = (string) $hexdec;
2545
2546 2
        if ($hexdec === '') {
2547 2
            return false;
2548
        }
2549
2550 2
        if (\preg_match('/^(?:\\\u|U\+|)([a-zA-Z0-9]{4,6})$/', $hexdec, $match)) {
2551 2
            return \intval($match[1], 16);
2552
        }
2553
2554 2
        return false;
2555
    }
2556
2557
    /**
2558
     * alias for "UTF8::html_entity_decode()"
2559
     *
2560
     * @param string $str
2561
     * @param int    $flags
2562
     * @param string $encoding
2563
     *
2564
     * @psalm-pure
2565
     *
2566
     * @return string
2567
     *
2568
     * @see        UTF8::html_entity_decode()
2569
     * @deprecated <p>please use "UTF8::html_entity_decode()"</p>
2570
     */
2571 2
    public static function html_decode(
2572
        string $str,
2573
        int $flags = null,
2574
        string $encoding = 'UTF-8'
2575
    ): string {
2576 2
        return self::html_entity_decode($str, $flags, $encoding);
2577
    }
2578
2579
    /**
2580
     * Converts a UTF-8 string to a series of HTML numbered entities.
2581
     *
2582
     * INFO: opposite to UTF8::html_decode()
2583
     *
2584
     * @param string $str              <p>The Unicode string to be encoded as numbered entities.</p>
2585
     * @param bool   $keep_ascii_chars [optional] <p>Keep ASCII chars.</p>
2586
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
2587
     *
2588
     * @psalm-pure
2589
     *
2590
     * @return string HTML numbered entities
2591
     */
2592 14
    public static function html_encode(
2593
        string $str,
2594
        bool $keep_ascii_chars = false,
2595
        string $encoding = 'UTF-8'
2596
    ): string {
2597 14
        if ($str === '') {
2598 4
            return '';
2599
        }
2600
2601 14
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2602 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2603
        }
2604
2605
        // INFO: http://stackoverflow.com/questions/35854535/better-explanation-of-convmap-in-mb-encode-numericentity
2606 14
        if (self::$SUPPORT['mbstring'] === true) {
2607 14
            $start_code = 0x00;
2608 14
            if ($keep_ascii_chars === true) {
2609 13
                $start_code = 0x80;
2610
            }
2611
2612 14
            if ($encoding === 'UTF-8') {
2613
                /** @var false|string|null $return - needed for PhpStan (stubs error) */
2614 14
                $return = \mb_encode_numericentity(
2615 14
                    $str,
2616 14
                    [$start_code, 0xfffff, 0, 0xfffff, 0]
2617
                );
2618 14
                if ($return !== null && $return !== false) {
2619 14
                    return $return;
2620
                }
2621
            }
2622
2623
            /** @var false|string|null $return - needed for PhpStan (stubs error) */
2624 4
            $return = \mb_encode_numericentity(
2625 4
                $str,
2626 4
                [$start_code, 0xfffff, 0, 0xfffff, 0],
2627 4
                $encoding
2628
            );
2629 4
            if ($return !== null && $return !== false) {
2630 4
                return $return;
2631
            }
2632
        }
2633
2634
        //
2635
        // fallback via vanilla php
2636
        //
2637
2638
        return \implode(
2639
            '',
2640
            \array_map(
2641
                static function (string $chr) use ($keep_ascii_chars, $encoding): string {
2642
                    return self::single_chr_html_encode($chr, $keep_ascii_chars, $encoding);
2643
                },
2644
                self::str_split($str)
2645
            )
2646
        );
2647
    }
2648
2649
    /**
2650
     * UTF-8 version of html_entity_decode()
2651
     *
2652
     * The reason we are not using html_entity_decode() by itself is because
2653
     * while it is not technically correct to leave out the semicolon
2654
     * at the end of an entity most browsers will still interpret the entity
2655
     * correctly. html_entity_decode() does not convert entities without
2656
     * semicolons, so we are left with our own little solution here. Bummer.
2657
     *
2658
     * Convert all HTML entities to their applicable characters
2659
     *
2660
     * INFO: opposite to UTF8::html_encode()
2661
     *
2662
     * @see http://php.net/manual/en/function.html-entity-decode.php
2663
     *
2664
     * @param string $str      <p>
2665
     *                         The input string.
2666
     *                         </p>
2667
     * @param int    $flags    [optional] <p>
2668
     *                         A bitmask of one or more of the following flags, which specify how to handle quotes
2669
     *                         and which document type to use. The default is ENT_COMPAT | ENT_HTML401.
2670
     *                         <table>
2671
     *                         Available <i>flags</i> constants
2672
     *                         <tr valign="top">
2673
     *                         <td>Constant Name</td>
2674
     *                         <td>Description</td>
2675
     *                         </tr>
2676
     *                         <tr valign="top">
2677
     *                         <td><b>ENT_COMPAT</b></td>
2678
     *                         <td>Will convert double-quotes and leave single-quotes alone.</td>
2679
     *                         </tr>
2680
     *                         <tr valign="top">
2681
     *                         <td><b>ENT_QUOTES</b></td>
2682
     *                         <td>Will convert both double and single quotes.</td>
2683
     *                         </tr>
2684
     *                         <tr valign="top">
2685
     *                         <td><b>ENT_NOQUOTES</b></td>
2686
     *                         <td>Will leave both double and single quotes unconverted.</td>
2687
     *                         </tr>
2688
     *                         <tr valign="top">
2689
     *                         <td><b>ENT_HTML401</b></td>
2690
     *                         <td>
2691
     *                         Handle code as HTML 4.01.
2692
     *                         </td>
2693
     *                         </tr>
2694
     *                         <tr valign="top">
2695
     *                         <td><b>ENT_XML1</b></td>
2696
     *                         <td>
2697
     *                         Handle code as XML 1.
2698
     *                         </td>
2699
     *                         </tr>
2700
     *                         <tr valign="top">
2701
     *                         <td><b>ENT_XHTML</b></td>
2702
     *                         <td>
2703
     *                         Handle code as XHTML.
2704
     *                         </td>
2705
     *                         </tr>
2706
     *                         <tr valign="top">
2707
     *                         <td><b>ENT_HTML5</b></td>
2708
     *                         <td>
2709
     *                         Handle code as HTML 5.
2710
     *                         </td>
2711
     *                         </tr>
2712
     *                         </table>
2713
     *                         </p>
2714
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2715
     *
2716
     * @psalm-pure
2717
     *
2718
     * @return string the decoded string
2719
     */
2720 51
    public static function html_entity_decode(
2721
        string $str,
2722
        int $flags = null,
2723
        string $encoding = 'UTF-8'
2724
    ): string {
2725
        if (
2726 51
            !isset($str[3]) // examples: &; || &x;
2727
            ||
2728 51
            \strpos($str, '&') === false // no "&"
2729
        ) {
2730 24
            return $str;
2731
        }
2732
2733 49
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2734 9
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2735
        }
2736
2737 49
        if ($flags === null) {
2738 11
            $flags = \ENT_QUOTES | \ENT_HTML5;
2739
        }
2740
2741
        if (
2742 49
            $encoding !== 'UTF-8'
2743
            &&
2744 49
            $encoding !== 'ISO-8859-1'
2745
            &&
2746 49
            $encoding !== 'WINDOWS-1252'
2747
            &&
2748 49
            self::$SUPPORT['mbstring'] === false
2749
        ) {
2750
            /**
2751
             * @psalm-suppress ImpureFunctionCall - is is only a warning
2752
             */
2753
            \trigger_error('UTF8::html_entity_decode() without mbstring cannot handle "' . $encoding . '" encoding', \E_USER_WARNING);
2754
        }
2755
2756
        do {
2757 49
            $str_compare = $str;
2758
2759 49
            if (\strpos($str, '&') !== false) {
2760 49
                if (\strpos($str, '&#') !== false) {
2761
                    // decode also numeric & UTF16 two byte entities
2762 41
                    $str = (string) \preg_replace(
2763 41
                        '/(&#(?:x0*[0-9a-fA-F]{2,6}(?![0-9a-fA-F;])|(?:0*\d{2,6}(?![0-9;]))))/S',
2764 41
                        '$1;',
2765 41
                        $str
2766
                    );
2767
                }
2768
2769 49
                $str = \html_entity_decode(
2770 49
                    $str,
2771 49
                    $flags,
2772 49
                    $encoding
2773
                );
2774
            }
2775 49
        } while ($str_compare !== $str);
2776
2777 49
        return $str;
2778
    }
2779
2780
    /**
2781
     * Create a escape html version of the string via "UTF8::htmlspecialchars()".
2782
     *
2783
     * @param string $str
2784
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
2785
     *
2786
     * @psalm-pure
2787
     *
2788
     * @return string
2789
     */
2790 6
    public static function html_escape(string $str, string $encoding = 'UTF-8'): string
2791
    {
2792 6
        return self::htmlspecialchars(
2793 6
            $str,
2794 6
            \ENT_QUOTES | \ENT_SUBSTITUTE,
2795 6
            $encoding
2796
        );
2797
    }
2798
2799
    /**
2800
     * Remove empty html-tag.
2801
     *
2802
     * e.g.: <tag></tag>
2803
     *
2804
     * @param string $str
2805
     *
2806
     * @psalm-pure
2807
     *
2808
     * @return string
2809
     */
2810 1
    public static function html_stripe_empty_tags(string $str): string
2811
    {
2812 1
        return (string) \preg_replace(
2813 1
            '/<[^\\/>]*?>\\s*?<\\/[^>]*?>/u',
2814 1
            '',
2815 1
            $str
2816
        );
2817
    }
2818
2819
    /**
2820
     * Convert all applicable characters to HTML entities: UTF-8 version of htmlentities()
2821
     *
2822
     * @see http://php.net/manual/en/function.htmlentities.php
2823
     *
2824
     * @param string $str           <p>
2825
     *                              The input string.
2826
     *                              </p>
2827
     * @param int    $flags         [optional] <p>
2828
     *                              A bitmask of one or more of the following flags, which specify how to handle
2829
     *                              quotes, invalid code unit sequences and the used document type. The default is
2830
     *                              ENT_COMPAT | ENT_HTML401.
2831
     *                              <table>
2832
     *                              Available <i>flags</i> constants
2833
     *                              <tr valign="top">
2834
     *                              <td>Constant Name</td>
2835
     *                              <td>Description</td>
2836
     *                              </tr>
2837
     *                              <tr valign="top">
2838
     *                              <td><b>ENT_COMPAT</b></td>
2839
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2840
     *                              </tr>
2841
     *                              <tr valign="top">
2842
     *                              <td><b>ENT_QUOTES</b></td>
2843
     *                              <td>Will convert both double and single quotes.</td>
2844
     *                              </tr>
2845
     *                              <tr valign="top">
2846
     *                              <td><b>ENT_NOQUOTES</b></td>
2847
     *                              <td>Will leave both double and single quotes unconverted.</td>
2848
     *                              </tr>
2849
     *                              <tr valign="top">
2850
     *                              <td><b>ENT_IGNORE</b></td>
2851
     *                              <td>
2852
     *                              Silently discard invalid code unit sequences instead of returning
2853
     *                              an empty string. Using this flag is discouraged as it
2854
     *                              may have security implications.
2855
     *                              </td>
2856
     *                              </tr>
2857
     *                              <tr valign="top">
2858
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2859
     *                              <td>
2860
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
2861
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
2862
     *                              string.
2863
     *                              </td>
2864
     *                              </tr>
2865
     *                              <tr valign="top">
2866
     *                              <td><b>ENT_DISALLOWED</b></td>
2867
     *                              <td>
2868
     *                              Replace invalid code points for the given document type with a
2869
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
2870
     *                              (otherwise) instead of leaving them as is. This may be useful, for
2871
     *                              instance, to ensure the well-formedness of XML documents with
2872
     *                              embedded external content.
2873
     *                              </td>
2874
     *                              </tr>
2875
     *                              <tr valign="top">
2876
     *                              <td><b>ENT_HTML401</b></td>
2877
     *                              <td>
2878
     *                              Handle code as HTML 4.01.
2879
     *                              </td>
2880
     *                              </tr>
2881
     *                              <tr valign="top">
2882
     *                              <td><b>ENT_XML1</b></td>
2883
     *                              <td>
2884
     *                              Handle code as XML 1.
2885
     *                              </td>
2886
     *                              </tr>
2887
     *                              <tr valign="top">
2888
     *                              <td><b>ENT_XHTML</b></td>
2889
     *                              <td>
2890
     *                              Handle code as XHTML.
2891
     *                              </td>
2892
     *                              </tr>
2893
     *                              <tr valign="top">
2894
     *                              <td><b>ENT_HTML5</b></td>
2895
     *                              <td>
2896
     *                              Handle code as HTML 5.
2897
     *                              </td>
2898
     *                              </tr>
2899
     *                              </table>
2900
     *                              </p>
2901
     * @param string $encoding      [optional] <p>
2902
     *                              Like <b>htmlspecialchars</b>,
2903
     *                              <b>htmlentities</b> takes an optional third argument
2904
     *                              <i>encoding</i> which defines encoding used in
2905
     *                              conversion.
2906
     *                              Although this argument is technically optional, you are highly
2907
     *                              encouraged to specify the correct value for your code.
2908
     *                              </p>
2909
     * @param bool   $double_encode [optional] <p>
2910
     *                              When <i>double_encode</i> is turned off PHP will not
2911
     *                              encode existing html entities. The default is to convert everything.
2912
     *                              </p>
2913
     *
2914
     * @psalm-pure
2915
     *
2916
     * @return string
2917
     *                <p>
2918
     *                The encoded string.
2919
     *                <br><br>
2920
     *                If the input <i>string</i> contains an invalid code unit
2921
     *                sequence within the given <i>encoding</i> an empty string
2922
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
2923
     *                <b>ENT_SUBSTITUTE</b> flags are set.
2924
     *                </p>
2925
     */
2926 9
    public static function htmlentities(
2927
        string $str,
2928
        int $flags = \ENT_COMPAT,
2929
        string $encoding = 'UTF-8',
2930
        bool $double_encode = true
2931
    ): string {
2932 9
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
2933 7
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
2934
        }
2935
2936 9
        $str = \htmlentities(
2937 9
            $str,
2938 9
            $flags,
2939 9
            $encoding,
2940 9
            $double_encode
2941
        );
2942
2943
        /**
2944
         * PHP doesn't replace a backslash to its html entity since this is something
2945
         * that's mostly used to escape characters when inserting in a database. Since
2946
         * we're using a decent database layer, we don't need this shit and we're replacing
2947
         * the double backslashes by its' html entity equivalent.
2948
         *
2949
         * https://github.com/forkcms/library/blob/master/spoon/filter/filter.php#L303
2950
         */
2951 9
        $str = \str_replace('\\', '&#92;', $str);
2952
2953 9
        return self::html_encode($str, true, $encoding);
2954
    }
2955
2956
    /**
2957
     * Convert only special characters to HTML entities: UTF-8 version of htmlspecialchars()
2958
     *
2959
     * INFO: Take a look at "UTF8::htmlentities()"
2960
     *
2961
     * @see http://php.net/manual/en/function.htmlspecialchars.php
2962
     *
2963
     * @param string $str           <p>
2964
     *                              The string being converted.
2965
     *                              </p>
2966
     * @param int    $flags         [optional] <p>
2967
     *                              A bitmask of one or more of the following flags, which specify how to handle
2968
     *                              quotes, invalid code unit sequences and the used document type. The default is
2969
     *                              ENT_COMPAT | ENT_HTML401.
2970
     *                              <table>
2971
     *                              Available <i>flags</i> constants
2972
     *                              <tr valign="top">
2973
     *                              <td>Constant Name</td>
2974
     *                              <td>Description</td>
2975
     *                              </tr>
2976
     *                              <tr valign="top">
2977
     *                              <td><b>ENT_COMPAT</b></td>
2978
     *                              <td>Will convert double-quotes and leave single-quotes alone.</td>
2979
     *                              </tr>
2980
     *                              <tr valign="top">
2981
     *                              <td><b>ENT_QUOTES</b></td>
2982
     *                              <td>Will convert both double and single quotes.</td>
2983
     *                              </tr>
2984
     *                              <tr valign="top">
2985
     *                              <td><b>ENT_NOQUOTES</b></td>
2986
     *                              <td>Will leave both double and single quotes unconverted.</td>
2987
     *                              </tr>
2988
     *                              <tr valign="top">
2989
     *                              <td><b>ENT_IGNORE</b></td>
2990
     *                              <td>
2991
     *                              Silently discard invalid code unit sequences instead of returning
2992
     *                              an empty string. Using this flag is discouraged as it
2993
     *                              may have security implications.
2994
     *                              </td>
2995
     *                              </tr>
2996
     *                              <tr valign="top">
2997
     *                              <td><b>ENT_SUBSTITUTE</b></td>
2998
     *                              <td>
2999
     *                              Replace invalid code unit sequences with a Unicode Replacement Character
3000
     *                              U+FFFD (UTF-8) or &#38;#38;#FFFD; (otherwise) instead of returning an empty
3001
     *                              string.
3002
     *                              </td>
3003
     *                              </tr>
3004
     *                              <tr valign="top">
3005
     *                              <td><b>ENT_DISALLOWED</b></td>
3006
     *                              <td>
3007
     *                              Replace invalid code points for the given document type with a
3008
     *                              Unicode Replacement Character U+FFFD (UTF-8) or &#38;#38;#FFFD;
3009
     *                              (otherwise) instead of leaving them as is. This may be useful, for
3010
     *                              instance, to ensure the well-formedness of XML documents with
3011
     *                              embedded external content.
3012
     *                              </td>
3013
     *                              </tr>
3014
     *                              <tr valign="top">
3015
     *                              <td><b>ENT_HTML401</b></td>
3016
     *                              <td>
3017
     *                              Handle code as HTML 4.01.
3018
     *                              </td>
3019
     *                              </tr>
3020
     *                              <tr valign="top">
3021
     *                              <td><b>ENT_XML1</b></td>
3022
     *                              <td>
3023
     *                              Handle code as XML 1.
3024
     *                              </td>
3025
     *                              </tr>
3026
     *                              <tr valign="top">
3027
     *                              <td><b>ENT_XHTML</b></td>
3028
     *                              <td>
3029
     *                              Handle code as XHTML.
3030
     *                              </td>
3031
     *                              </tr>
3032
     *                              <tr valign="top">
3033
     *                              <td><b>ENT_HTML5</b></td>
3034
     *                              <td>
3035
     *                              Handle code as HTML 5.
3036
     *                              </td>
3037
     *                              </tr>
3038
     *                              </table>
3039
     *                              </p>
3040
     * @param string $encoding      [optional] <p>
3041
     *                              Defines encoding used in conversion.
3042
     *                              </p>
3043
     *                              <p>
3044
     *                              For the purposes of this function, the encodings
3045
     *                              ISO-8859-1, ISO-8859-15,
3046
     *                              UTF-8, cp866,
3047
     *                              cp1251, cp1252, and
3048
     *                              KOI8-R are effectively equivalent, provided the
3049
     *                              <i>string</i> itself is valid for the encoding, as
3050
     *                              the characters affected by <b>htmlspecialchars</b> occupy
3051
     *                              the same positions in all of these encodings.
3052
     *                              </p>
3053
     * @param bool   $double_encode [optional] <p>
3054
     *                              When <i>double_encode</i> is turned off PHP will not
3055
     *                              encode existing html entities, the default is to convert everything.
3056
     *                              </p>
3057
     *
3058
     * @psalm-pure
3059
     *
3060
     * @return string the converted string.
3061
     *                </p>
3062
     *                <p>
3063
     *                If the input <i>string</i> contains an invalid code unit
3064
     *                sequence within the given <i>encoding</i> an empty string
3065
     *                will be returned, unless either the <b>ENT_IGNORE</b> or
3066
     *                <b>ENT_SUBSTITUTE</b> flags are set
3067
     */
3068 8
    public static function htmlspecialchars(
3069
        string $str,
3070
        int $flags = \ENT_COMPAT,
3071
        string $encoding = 'UTF-8',
3072
        bool $double_encode = true
3073
    ): string {
3074 8
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
3075 8
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
3076
        }
3077
3078 8
        return \htmlspecialchars(
3079 8
            $str,
3080 8
            $flags,
3081 8
            $encoding,
3082 8
            $double_encode
3083
        );
3084
    }
3085
3086
    /**
3087
     * Checks whether iconv is available on the server.
3088
     *
3089
     * @psalm-pure
3090
     *
3091
     * @return bool
3092
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3093
     */
3094
    public static function iconv_loaded(): bool
3095
    {
3096
        return \extension_loaded('iconv');
3097
    }
3098
3099
    /**
3100
     * alias for "UTF8::decimal_to_chr()"
3101
     *
3102
     * @param mixed $int
3103
     *
3104
     * @psalm-pure
3105
     *
3106
     * @return string
3107
     *
3108
     * @see        UTF8::decimal_to_chr()
3109
     * @deprecated <p>please use "UTF8::decimal_to_chr()"</p>
3110
     */
3111 4
    public static function int_to_chr($int): string
3112
    {
3113 4
        return self::decimal_to_chr($int);
3114
    }
3115
3116
    /**
3117
     * Converts Integer to hexadecimal U+xxxx code point representation.
3118
     *
3119
     * INFO: opposite to UTF8::hex_to_int()
3120
     *
3121
     * @param int    $int    <p>The integer to be converted to hexadecimal code point.</p>
3122
     * @param string $prefix [optional]
3123
     *
3124
     * @psalm-pure
3125
     *
3126
     * @return string the code point, or empty string on failure
3127
     */
3128 6
    public static function int_to_hex(int $int, string $prefix = 'U+'): string
3129
    {
3130 6
        $hex = \dechex($int);
3131
3132 6
        $hex = (\strlen($hex) < 4 ? \substr('0000' . $hex, -4) : $hex);
3133
3134 6
        return $prefix . $hex . '';
3135
    }
3136
3137
    /**
3138
     * Checks whether intl-char is available on the server.
3139
     *
3140
     * @psalm-pure
3141
     *
3142
     * @return bool
3143
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3144
     */
3145
    public static function intlChar_loaded(): bool
3146
    {
3147
        return \class_exists('IntlChar');
3148
    }
3149
3150
    /**
3151
     * Checks whether intl is available on the server.
3152
     *
3153
     * @psalm-pure
3154
     *
3155
     * @return bool
3156
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
3157
     */
3158 5
    public static function intl_loaded(): bool
3159
    {
3160 5
        return \extension_loaded('intl');
3161
    }
3162
3163
    /**
3164
     * alias for "UTF8::is_ascii()"
3165
     *
3166
     * @param string $str
3167
     *
3168
     * @psalm-pure
3169
     *
3170
     * @return bool
3171
     *
3172
     * @see        UTF8::is_ascii()
3173
     * @deprecated <p>please use "UTF8::is_ascii()"</p>
3174
     */
3175 2
    public static function isAscii(string $str): bool
3176
    {
3177 2
        return ASCII::is_ascii($str);
3178
    }
3179
3180
    /**
3181
     * alias for "UTF8::is_base64()"
3182
     *
3183
     * @param string $str
3184
     *
3185
     * @psalm-pure
3186
     *
3187
     * @return bool
3188
     *
3189
     * @see        UTF8::is_base64()
3190
     * @deprecated <p>please use "UTF8::is_base64()"</p>
3191
     */
3192 2
    public static function isBase64($str): bool
3193
    {
3194 2
        return self::is_base64($str);
3195
    }
3196
3197
    /**
3198
     * alias for "UTF8::is_binary()"
3199
     *
3200
     * @param mixed $str
3201
     * @param bool  $strict
3202
     *
3203
     * @psalm-pure
3204
     *
3205
     * @return bool
3206
     *
3207
     * @see        UTF8::is_binary()
3208
     * @deprecated <p>please use "UTF8::is_binary()"</p>
3209
     */
3210 4
    public static function isBinary($str, $strict = false): bool
3211
    {
3212 4
        return self::is_binary($str, $strict);
3213
    }
3214
3215
    /**
3216
     * alias for "UTF8::is_bom()"
3217
     *
3218
     * @param string $utf8_chr
3219
     *
3220
     * @psalm-pure
3221
     *
3222
     * @return bool
3223
     *
3224
     * @see        UTF8::is_bom()
3225
     * @deprecated <p>please use "UTF8::is_bom()"</p>
3226
     */
3227 2
    public static function isBom(string $utf8_chr): bool
3228
    {
3229 2
        return self::is_bom($utf8_chr);
3230
    }
3231
3232
    /**
3233
     * alias for "UTF8::is_html()"
3234
     *
3235
     * @param string $str
3236
     *
3237
     * @psalm-pure
3238
     *
3239
     * @return bool
3240
     *
3241
     * @see        UTF8::is_html()
3242
     * @deprecated <p>please use "UTF8::is_html()"</p>
3243
     */
3244 2
    public static function isHtml(string $str): bool
3245
    {
3246 2
        return self::is_html($str);
3247
    }
3248
3249
    /**
3250
     * alias for "UTF8::is_json()"
3251
     *
3252
     * @param string $str
3253
     *
3254
     * @return bool
3255
     *
3256
     * @see        UTF8::is_json()
3257
     * @deprecated <p>please use "UTF8::is_json()"</p>
3258
     */
3259
    public static function isJson(string $str): bool
3260
    {
3261
        return self::is_json($str);
3262
    }
3263
3264
    /**
3265
     * alias for "UTF8::is_utf16()"
3266
     *
3267
     * @param mixed $str
3268
     *
3269
     * @psalm-pure
3270
     *
3271
     * @return false|int
3272
     *                   <strong>false</strong> if is't not UTF16,<br>
3273
     *                   <strong>1</strong> for UTF-16LE,<br>
3274
     *                   <strong>2</strong> for UTF-16BE
3275
     *
3276
     * @see        UTF8::is_utf16()
3277
     * @deprecated <p>please use "UTF8::is_utf16()"</p>
3278
     */
3279 2
    public static function isUtf16($str)
3280
    {
3281 2
        return self::is_utf16($str);
3282
    }
3283
3284
    /**
3285
     * alias for "UTF8::is_utf32()"
3286
     *
3287
     * @param mixed $str
3288
     *
3289
     * @psalm-pure
3290
     *
3291
     * @return false|int
3292
     *                   <strong>false</strong> if is't not UTF16,
3293
     *                   <strong>1</strong> for UTF-32LE,
3294
     *                   <strong>2</strong> for UTF-32BE
3295
     *
3296
     * @see        UTF8::is_utf32()
3297
     * @deprecated <p>please use "UTF8::is_utf32()"</p>
3298
     */
3299 2
    public static function isUtf32($str)
3300
    {
3301 2
        return self::is_utf32($str);
3302
    }
3303
3304
    /**
3305
     * alias for "UTF8::is_utf8()"
3306
     *
3307
     * @param string $str
3308
     * @param bool   $strict
3309
     *
3310
     * @psalm-pure
3311
     *
3312
     * @return bool
3313
     *
3314
     * @see        UTF8::is_utf8()
3315
     * @deprecated <p>please use "UTF8::is_utf8()"</p>
3316
     */
3317 17
    public static function isUtf8($str, $strict = false): bool
3318
    {
3319 17
        return self::is_utf8($str, $strict);
3320
    }
3321
3322
    /**
3323
     * Returns true if the string contains only alphabetic chars, false otherwise.
3324
     *
3325
     * @param string $str <p>The input string.</p>
3326
     *
3327
     * @psalm-pure
3328
     *
3329
     * @return bool
3330
     *              <p>Whether or not $str contains only alphabetic chars.</p>
3331
     */
3332 10
    public static function is_alpha(string $str): bool
3333
    {
3334 10
        if (self::$SUPPORT['mbstring'] === true) {
3335
            /** @noinspection PhpComposerExtensionStubsInspection */
3336 10
            return \mb_ereg_match('^[[:alpha:]]*$', $str);
3337
        }
3338
3339
        return self::str_matches_pattern($str, '^[[:alpha:]]*$');
3340
    }
3341
3342
    /**
3343
     * Returns true if the string contains only alphabetic and numeric chars, false otherwise.
3344
     *
3345
     * @param string $str <p>The input string.</p>
3346
     *
3347
     * @psalm-pure
3348
     *
3349
     * @return bool
3350
     *              <p>Whether or not $str contains only alphanumeric chars.</p>
3351
     */
3352 13
    public static function is_alphanumeric(string $str): bool
3353
    {
3354 13
        if (self::$SUPPORT['mbstring'] === true) {
3355
            /** @noinspection PhpComposerExtensionStubsInspection */
3356 13
            return \mb_ereg_match('^[[:alnum:]]*$', $str);
3357
        }
3358
3359
        return self::str_matches_pattern($str, '^[[:alnum:]]*$');
3360
    }
3361
3362
    /**
3363
     * Returns true if the string contains only punctuation chars, false otherwise.
3364
     *
3365
     * @param string $str <p>The input string.</p>
3366
     *
3367
     * @psalm-pure
3368
     *
3369
     * @return bool
3370
     *              <p>Whether or not $str contains only punctuation chars.</p>
3371
     */
3372 10
    public static function is_punctuation(string $str): bool
3373
    {
3374 10
        return self::str_matches_pattern($str, '^[[:punct:]]*$');
3375
    }
3376
3377
    /**
3378
     * Returns true if the string contains only printable (non-invisible) chars, false otherwise.
3379
     *
3380
     * @param string $str <p>The input string.</p>
3381
     *
3382
     * @psalm-pure
3383
     *
3384
     * @return bool
3385
     *              <p>Whether or not $str contains only printable (non-invisible) chars.</p>
3386
     */
3387 1
    public static function is_printable(string $str): bool
3388
    {
3389 1
        return self::remove_invisible_characters($str) === $str;
3390
    }
3391
3392
    /**
3393
     * Checks if a string is 7 bit ASCII.
3394
     *
3395
     * @param string $str <p>The string to check.</p>
3396
     *
3397
     * @psalm-pure
3398
     *
3399
     * @return bool
3400
     *              <p>
3401
     *              <strong>true</strong> if it is ASCII<br>
3402
     *              <strong>false</strong> otherwise
3403
     *              </p>
3404
     */
3405 8
    public static function is_ascii(string $str): bool
3406
    {
3407 8
        return ASCII::is_ascii($str);
3408
    }
3409
3410
    /**
3411
     * Returns true if the string is base64 encoded, false otherwise.
3412
     *
3413
     * @param mixed|string $str                   <p>The input string.</p>
3414
     * @param bool         $empty_string_is_valid [optional] <p>Is an empty string valid base64 or not?</p>
3415
     *
3416
     * @psalm-pure
3417
     *
3418
     * @return bool whether or not $str is base64 encoded
3419
     */
3420 16
    public static function is_base64($str, $empty_string_is_valid = false): bool
3421
    {
3422
        if (
3423 16
            $empty_string_is_valid === false
3424
            &&
3425 16
            $str === ''
3426
        ) {
3427 3
            return false;
3428
        }
3429
3430
        /**
3431
         * @psalm-suppress RedundantConditionGivenDocblockType
3432
         */
3433 15
        if (\is_string($str) === false) {
3434 2
            return false;
3435
        }
3436
3437 15
        $base64String = \base64_decode($str, true);
3438
3439 15
        return $base64String !== false && \base64_encode($base64String) === $str;
3440
    }
3441
3442
    /**
3443
     * Check if the input is binary... (is look like a hack).
3444
     *
3445
     * @param mixed $input
3446
     * @param bool  $strict
3447
     *
3448
     * @psalm-pure
3449
     *
3450
     * @return bool
3451
     */
3452 39
    public static function is_binary($input, bool $strict = false): bool
3453
    {
3454 39
        $input = (string) $input;
3455 39
        if ($input === '') {
3456 10
            return false;
3457
        }
3458
3459 39
        if (\preg_match('~^[01]+$~', $input)) {
3460 13
            return true;
3461
        }
3462
3463 39
        $ext = self::get_file_type($input);
3464 39
        if ($ext['type'] === 'binary') {
3465 7
            return true;
3466
        }
3467
3468 38
        $test_length = \strlen($input);
3469 38
        $test_null_counting = \substr_count($input, "\x0", 0, $test_length);
3470 38
        if (($test_null_counting / $test_length) > 0.25) {
3471 15
            return true;
3472
        }
3473
3474 34
        if ($strict === true) {
3475 34
            if (self::$SUPPORT['finfo'] === false) {
3476
                throw new \RuntimeException('ext-fileinfo: is not installed');
3477
            }
3478
3479
            /**
3480
             * @noinspection   PhpComposerExtensionStubsInspection
3481
             * @psalm-suppress ImpureMethodCall - it will return the same result for the same file ...
3482
             */
3483 34
            $finfo_encoding = (new \finfo(\FILEINFO_MIME_ENCODING))->buffer($input);
3484 34
            if ($finfo_encoding && $finfo_encoding === 'binary') {
3485 15
                return true;
3486
            }
3487
        }
3488
3489 30
        return false;
3490
    }
3491
3492
    /**
3493
     * Check if the file is binary.
3494
     *
3495
     * @param string $file
3496
     *
3497
     * @return bool
3498
     */
3499 6
    public static function is_binary_file($file): bool
3500
    {
3501
        // init
3502 6
        $block = '';
3503
3504 6
        $fp = \fopen($file, 'rb');
3505 6
        if (\is_resource($fp)) {
3506 6
            $block = \fread($fp, 512);
3507 6
            \fclose($fp);
3508
        }
3509
3510 6
        if ($block === '') {
3511 2
            return false;
3512
        }
3513
3514 6
        return self::is_binary($block, true);
3515
    }
3516
3517
    /**
3518
     * Returns true if the string contains only whitespace chars, false otherwise.
3519
     *
3520
     * @param string $str <p>The input string.</p>
3521
     *
3522
     * @psalm-pure
3523
     *
3524
     * @return bool
3525
     *              <p>Whether or not $str contains only whitespace characters.</p>
3526
     */
3527 15
    public static function is_blank(string $str): bool
3528
    {
3529 15
        if (self::$SUPPORT['mbstring'] === true) {
3530
            /** @noinspection PhpComposerExtensionStubsInspection */
3531 15
            return \mb_ereg_match('^[[:space:]]*$', $str);
3532
        }
3533
3534
        return self::str_matches_pattern($str, '^[[:space:]]*$');
3535
    }
3536
3537
    /**
3538
     * Checks if the given string is equal to any "Byte Order Mark".
3539
     *
3540
     * WARNING: Use "UTF8::string_has_bom()" if you will check BOM in a string.
3541
     *
3542
     * @param string $str <p>The input string.</p>
3543
     *
3544
     * @psalm-pure
3545
     *
3546
     * @return bool
3547
     *              <p><strong>true</strong> if the $utf8_chr is Byte Order Mark, <strong>false</strong> otherwise.</p>
3548
     */
3549 2
    public static function is_bom($str): bool
3550
    {
3551
        /** @noinspection PhpUnusedLocalVariableInspection */
3552 2
        foreach (self::$BOM as $bom_string => &$bom_byte_length) {
3553 2
            if ($str === $bom_string) {
3554 2
                return true;
3555
            }
3556
        }
3557
3558 2
        return false;
3559
    }
3560
3561
    /**
3562
     * Determine whether the string is considered to be empty.
3563
     *
3564
     * A variable is considered empty if it does not exist or if its value equals FALSE.
3565
     * empty() does not generate a warning if the variable does not exist.
3566
     *
3567
     * @param mixed $str
3568
     *
3569
     * @psalm-pure
3570
     *
3571
     * @return bool whether or not $str is empty()
3572
     */
3573
    public static function is_empty($str): bool
3574
    {
3575
        return empty($str);
3576
    }
3577
3578
    /**
3579
     * Returns true if the string contains only hexadecimal chars, false otherwise.
3580
     *
3581
     * @param string $str <p>The input string.</p>
3582
     *
3583
     * @psalm-pure
3584
     *
3585
     * @return bool
3586
     *              <p>Whether or not $str contains only hexadecimal chars.</p>
3587
     */
3588 13
    public static function is_hexadecimal(string $str): bool
3589
    {
3590 13
        if (self::$SUPPORT['mbstring'] === true) {
3591
            /** @noinspection PhpComposerExtensionStubsInspection */
3592 13
            return \mb_ereg_match('^[[:xdigit:]]*$', $str);
3593
        }
3594
3595
        return self::str_matches_pattern($str, '^[[:xdigit:]]*$');
3596
    }
3597
3598
    /**
3599
     * Check if the string contains any HTML tags.
3600
     *
3601
     * @param string $str <p>The input string.</p>
3602
     *
3603
     * @psalm-pure
3604
     *
3605
     * @return bool
3606
     *              <p>Whether or not $str contains html elements.</p>
3607
     */
3608 3
    public static function is_html(string $str): bool
3609
    {
3610 3
        if ($str === '') {
3611 3
            return false;
3612
        }
3613
3614
        // init
3615 3
        $matches = [];
3616
3617 3
        $str = self::emoji_encode($str); // hack for emoji support :/
3618
3619 3
        \preg_match("/<\\/?\\w+(?:(?:\\s+\\w+(?:\\s*=\\s*(?:\".*?\"|'.*?'|[^'\">\\s]+))?)*\\s*|\\s*)\\/?>/u", $str, $matches);
3620
3621 3
        return $matches !== [];
3622
    }
3623
3624
    /**
3625
     * Check if $url is an correct url.
3626
     *
3627
     * @param string $url
3628
     * @param bool   $disallow_localhost
3629
     *
3630
     * @psalm-pure
3631
     *
3632
     * @return bool
3633
     */
3634 1
    public static function is_url(string $url, bool $disallow_localhost = false): bool
3635
    {
3636 1
        $url = (string) $url;
3637 1
        if (!$url) {
3638 1
            return false;
3639
        }
3640
3641
        // WARNING: keep this as hack protection
3642 1
        if (self::str_istarts_with_any($url, ['http://', 'https://']) === false) {
3643 1
            return false;
3644
        }
3645
3646
        // e.g. -> the server itself connect to "https://foo.localhost/phpmyadmin/...
3647 1
        if ($disallow_localhost) {
3648
            if (self::str_istarts_with_any(
3649
                $url,
3650
                [
3651
                    'http://localhost',
3652
                    'https://localhost',
3653
                    'http://127.0.0.1',
3654
                    'https://127.0.0.1',
3655
                    'http://::1',
3656
                    'https://::1',
3657
                ]
3658
            )) {
3659
                return false;
3660
            }
3661
3662
            $regex = '/^(?:http(?:s)?:\/\/).*?(?:\.localhost)/iu';
3663
            if (\preg_match($regex, $url)) {
3664
                return false;
3665
            }
3666
        }
3667
3668
        // INFO: this is needed for e.g. "http://müller.de/" (internationalized domain names) and non ASCII-parameters
3669 1
        $regex = '/^(?:http(?:s)?:\\/\\/)(?:[\p{L}0-9][\p{L}0-9_-]*(?:\\.[\p{L}0-9][\p{L}0-9_-]*))(?:\\d+)?(?:\\/\\.*)?/iu';
3670 1
        if (\preg_match($regex, $url)) {
3671 1
            return true;
3672
        }
3673
3674 1
        return \filter_var($url, \FILTER_VALIDATE_URL, ['flags' => \FILTER_FLAG_SCHEME_REQUIRED | \FILTER_FLAG_HOST_REQUIRED]) !== false;
0 ignored issues
show
introduced by
The constant FILTER_FLAG_SCHEME_REQUIRED has been deprecated: 7.3.0 ( Ignorable by Annotation )

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

3674
        return \filter_var($url, \FILTER_VALIDATE_URL, ['flags' => /** @scrutinizer ignore-deprecated */ \FILTER_FLAG_SCHEME_REQUIRED | \FILTER_FLAG_HOST_REQUIRED]) !== false;
Loading history...
introduced by
The constant FILTER_FLAG_HOST_REQUIRED has been deprecated: 7.3.0 ( Ignorable by Annotation )

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

3674
        return \filter_var($url, \FILTER_VALIDATE_URL, ['flags' => \FILTER_FLAG_SCHEME_REQUIRED | /** @scrutinizer ignore-deprecated */ \FILTER_FLAG_HOST_REQUIRED]) !== false;
Loading history...
3675
    }
3676
3677
    /**
3678
     * Try to check if "$str" is a JSON-string.
3679
     *
3680
     * @param string $str                                    <p>The input string.</p>
3681
     * @param bool   $only_array_or_object_results_are_valid [optional] <p>Only array and objects are valid json
3682
     *                                                       results.</p>
3683
     *
3684
     * @return bool
3685
     *              <p>Whether or not the $str is in JSON format.</p>
3686
     */
3687 42
    public static function is_json(
3688
        string $str,
3689
        $only_array_or_object_results_are_valid = true
3690
    ): bool {
3691 42
        if ($str === '') {
3692 4
            return false;
3693
        }
3694
3695 40
        if (self::$SUPPORT['json'] === false) {
3696
            throw new \RuntimeException('ext-json: is not installed');
3697
        }
3698
3699 40
        $json = self::json_decode($str);
3700 40
        if ($json === null && \strtoupper($str) !== 'NULL') {
3701 18
            return false;
3702
        }
3703
3704
        if (
3705 24
            $only_array_or_object_results_are_valid === true
3706
            &&
3707 24
            \is_object($json) === false
3708
            &&
3709 24
            \is_array($json) === false
3710
        ) {
3711 5
            return false;
3712
        }
3713
3714
        /** @noinspection PhpComposerExtensionStubsInspection */
3715 19
        return \json_last_error() === \JSON_ERROR_NONE;
3716
    }
3717
3718
    /**
3719
     * @param string $str <p>The input string.</p>
3720
     *
3721
     * @psalm-pure
3722
     *
3723
     * @return bool
3724
     *              <p>Whether or not $str contains only lowercase chars.</p>
3725
     */
3726 8
    public static function is_lowercase(string $str): bool
3727
    {
3728 8
        if (self::$SUPPORT['mbstring'] === true) {
3729
            /** @noinspection PhpComposerExtensionStubsInspection */
3730 8
            return \mb_ereg_match('^[[:lower:]]*$', $str);
3731
        }
3732
3733
        return self::str_matches_pattern($str, '^[[:lower:]]*$');
3734
    }
3735
3736
    /**
3737
     * Returns true if the string is serialized, false otherwise.
3738
     *
3739
     * @param string $str <p>The input string.</p>
3740
     *
3741
     * @psalm-pure
3742
     *
3743
     * @return bool
3744
     *              <p>Whether or not $str is serialized.</p>
3745
     */
3746 7
    public static function is_serialized(string $str): bool
3747
    {
3748 7
        if ($str === '') {
3749 1
            return false;
3750
        }
3751
3752
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
3753
        /** @noinspection UnserializeExploitsInspection */
3754 6
        return $str === 'b:0;'
3755
               ||
3756 6
               @\unserialize($str) !== false;
3757
    }
3758
3759
    /**
3760
     * Returns true if the string contains only lower case chars, false
3761
     * otherwise.
3762
     *
3763
     * @param string $str <p>The input string.</p>
3764
     *
3765
     * @psalm-pure
3766
     *
3767
     * @return bool
3768
     *              <p>Whether or not $str contains only lower case characters.</p>
3769
     */
3770 8
    public static function is_uppercase(string $str): bool
3771
    {
3772 8
        if (self::$SUPPORT['mbstring'] === true) {
3773
            /** @noinspection PhpComposerExtensionStubsInspection */
3774 8
            return \mb_ereg_match('^[[:upper:]]*$', $str);
3775
        }
3776
3777
        return self::str_matches_pattern($str, '^[[:upper:]]*$');
3778
    }
3779
3780
    /**
3781
     * Check if the string is UTF-16.
3782
     *
3783
     * @param mixed $str                       <p>The input string.</p>
3784
     * @param bool  $check_if_string_is_binary
3785
     *
3786
     * @psalm-pure
3787
     *
3788
     * @return false|int
3789
     *                   <strong>false</strong> if is't not UTF-16,<br>
3790
     *                   <strong>1</strong> for UTF-16LE,<br>
3791
     *                   <strong>2</strong> for UTF-16BE
3792
     */
3793 22
    public static function is_utf16($str, $check_if_string_is_binary = true)
3794
    {
3795
        // init
3796 22
        $str = (string) $str;
3797 22
        $str_chars = [];
3798
3799
        if (
3800 22
            $check_if_string_is_binary === true
3801
            &&
3802 22
            self::is_binary($str, true) === false
3803
        ) {
3804 2
            return false;
3805
        }
3806
3807 22
        if (self::$SUPPORT['mbstring'] === false) {
3808
            /**
3809
             * @psalm-suppress ImpureFunctionCall - is is only a warning
3810
             */
3811 3
            \trigger_error('UTF8::is_utf16() without mbstring may did not work correctly', \E_USER_WARNING);
3812
        }
3813
3814 22
        $str = self::remove_bom($str);
3815
3816 22
        $maybe_utf16le = 0;
3817 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16LE');
3818 22
        if ($test) {
3819 15
            $test2 = \mb_convert_encoding($test, 'UTF-16LE', 'UTF-8');
3820 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16LE');
3821 15
            if ($test3 === $test) {
3822
                /**
3823
                 * @psalm-suppress RedundantCondition
3824
                 */
3825 15
                if ($str_chars === []) {
3826 15
                    $str_chars = self::count_chars($str, true, false);
3827
                }
3828 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...
3829 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3830 15
                        ++$maybe_utf16le;
3831
                    }
3832
                }
3833 15
                unset($test3charEmpty);
3834
            }
3835
        }
3836
3837 22
        $maybe_utf16be = 0;
3838 22
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-16BE');
3839 22
        if ($test) {
3840 15
            $test2 = \mb_convert_encoding($test, 'UTF-16BE', 'UTF-8');
3841 15
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-16BE');
3842 15
            if ($test3 === $test) {
3843 15
                if ($str_chars === []) {
3844 7
                    $str_chars = self::count_chars($str, true, false);
3845
                }
3846 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...
3847 15
                    if (\in_array($test3char, $str_chars, true) === true) {
3848 15
                        ++$maybe_utf16be;
3849
                    }
3850
                }
3851 15
                unset($test3charEmpty);
3852
            }
3853
        }
3854
3855 22
        if ($maybe_utf16be !== $maybe_utf16le) {
3856 7
            if ($maybe_utf16le > $maybe_utf16be) {
3857 5
                return 1;
3858
            }
3859
3860 6
            return 2;
3861
        }
3862
3863 18
        return false;
3864
    }
3865
3866
    /**
3867
     * Check if the string is UTF-32.
3868
     *
3869
     * @param mixed $str                       <p>The input string.</p>
3870
     * @param bool  $check_if_string_is_binary
3871
     *
3872
     * @psalm-pure
3873
     *
3874
     * @return false|int
3875
     *                   <strong>false</strong> if is't not UTF-32,<br>
3876
     *                   <strong>1</strong> for UTF-32LE,<br>
3877
     *                   <strong>2</strong> for UTF-32BE
3878
     */
3879 20
    public static function is_utf32($str, $check_if_string_is_binary = true)
3880
    {
3881
        // init
3882 20
        $str = (string) $str;
3883 20
        $str_chars = [];
3884
3885
        if (
3886 20
            $check_if_string_is_binary === true
3887
            &&
3888 20
            self::is_binary($str, true) === false
3889
        ) {
3890 2
            return false;
3891
        }
3892
3893 20
        if (self::$SUPPORT['mbstring'] === false) {
3894
            /**
3895
             * @psalm-suppress ImpureFunctionCall - is is only a warning
3896
             */
3897 3
            \trigger_error('UTF8::is_utf32() without mbstring may did not work correctly', \E_USER_WARNING);
3898
        }
3899
3900 20
        $str = self::remove_bom($str);
3901
3902 20
        $maybe_utf32le = 0;
3903 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32LE');
3904 20
        if ($test) {
3905 13
            $test2 = \mb_convert_encoding($test, 'UTF-32LE', 'UTF-8');
3906 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32LE');
3907 13
            if ($test3 === $test) {
3908
                /**
3909
                 * @psalm-suppress RedundantCondition
3910
                 */
3911 13
                if ($str_chars === []) {
3912 13
                    $str_chars = self::count_chars($str, true, false);
3913
                }
3914 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...
3915 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3916 13
                        ++$maybe_utf32le;
3917
                    }
3918
                }
3919 13
                unset($test3charEmpty);
3920
            }
3921
        }
3922
3923 20
        $maybe_utf32be = 0;
3924 20
        $test = \mb_convert_encoding($str, 'UTF-8', 'UTF-32BE');
3925 20
        if ($test) {
3926 13
            $test2 = \mb_convert_encoding($test, 'UTF-32BE', 'UTF-8');
3927 13
            $test3 = \mb_convert_encoding($test2, 'UTF-8', 'UTF-32BE');
3928 13
            if ($test3 === $test) {
3929 13
                if ($str_chars === []) {
3930 7
                    $str_chars = self::count_chars($str, true, false);
3931
                }
3932 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...
3933 13
                    if (\in_array($test3char, $str_chars, true) === true) {
3934 13
                        ++$maybe_utf32be;
3935
                    }
3936
                }
3937 13
                unset($test3charEmpty);
3938
            }
3939
        }
3940
3941 20
        if ($maybe_utf32be !== $maybe_utf32le) {
3942 3
            if ($maybe_utf32le > $maybe_utf32be) {
3943 2
                return 1;
3944
            }
3945
3946 3
            return 2;
3947
        }
3948
3949 20
        return false;
3950
    }
3951
3952
    /**
3953
     * Checks whether the passed input contains only byte sequences that appear valid UTF-8.
3954
     *
3955
     * @param int|string|string[]|null $str    <p>The input to be checked.</p>
3956
     * @param bool                     $strict <p>Check also if the string is not UTF-16 or UTF-32.</p>
3957
     *
3958
     * @psalm-pure
3959
     *
3960
     * @return bool
3961
     */
3962 82
    public static function is_utf8($str, bool $strict = false): bool
3963
    {
3964 82
        if (\is_array($str) === true) {
3965 2
            foreach ($str as &$v) {
3966 2
                if (self::is_utf8($v, $strict) === false) {
3967 2
                    return false;
3968
                }
3969
            }
3970
3971
            return true;
3972
        }
3973
3974 82
        return self::is_utf8_string((string) $str, $strict);
3975
    }
3976
3977
    /**
3978
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
3979
     * Decodes a JSON string
3980
     *
3981
     * @see http://php.net/manual/en/function.json-decode.php
3982
     *
3983
     * @param string $json    <p>
3984
     *                        The <i>json</i> string being decoded.
3985
     *                        </p>
3986
     *                        <p>
3987
     *                        This function only works with UTF-8 encoded strings.
3988
     *                        </p>
3989
     *                        <p>PHP implements a superset of
3990
     *                        JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
3991
     *                        only supports these values when they are nested inside an array or an object.
3992
     *                        </p>
3993
     * @param bool   $assoc   [optional] <p>
3994
     *                        When <b>TRUE</b>, returned objects will be converted into
3995
     *                        associative arrays.
3996
     *                        </p>
3997
     * @param int    $depth   [optional] <p>
3998
     *                        User specified recursion depth.
3999
     *                        </p>
4000
     * @param int    $options [optional] <p>
4001
     *                        Bitmask of JSON decode options. Currently only
4002
     *                        <b>JSON_BIGINT_AS_STRING</b>
4003
     *                        is supported (default is to cast large integers as floats)
4004
     *                        </p>
4005
     *
4006
     * @psalm-pure
4007
     *
4008
     * @return mixed
4009
     *               The value encoded in <i>json</i> in appropriate PHP type. Values true, false and
4010
     *               null (case-insensitive) are returned as <b>TRUE</b>, <b>FALSE</b> and <b>NULL</b> respectively.
4011
     *               <b>NULL</b> is returned if the <i>json</i> cannot be decoded or if the encoded data
4012
     *               is deeper than the recursion limit.
4013
     */
4014 43
    public static function json_decode(
4015
        string $json,
4016
        bool $assoc = false,
4017
        int $depth = 512,
4018
        int $options = 0
4019
    ) {
4020 43
        $json = self::filter($json);
4021
4022 43
        if (self::$SUPPORT['json'] === false) {
4023
            throw new \RuntimeException('ext-json: is not installed');
4024
        }
4025
4026
        /** @noinspection PhpComposerExtensionStubsInspection */
4027 43
        return \json_decode($json, $assoc, $depth, $options);
4028
    }
4029
4030
    /**
4031
     * (PHP 5 &gt;= 5.2.0, PECL json &gt;= 1.2.0)<br/>
4032
     * Returns the JSON representation of a value.
4033
     *
4034
     * @see http://php.net/manual/en/function.json-encode.php
4035
     *
4036
     * @param mixed $value   <p>
4037
     *                       The <i>value</i> being encoded. Can be any type except
4038
     *                       a resource.
4039
     *                       </p>
4040
     *                       <p>
4041
     *                       All string data must be UTF-8 encoded.
4042
     *                       </p>
4043
     *                       <p>PHP implements a superset of
4044
     *                       JSON - it will also encode and decode scalar types and <b>NULL</b>. The JSON standard
4045
     *                       only supports these values when they are nested inside an array or an object.
4046
     *                       </p>
4047
     * @param int   $options [optional] <p>
4048
     *                       Bitmask consisting of <b>JSON_HEX_QUOT</b>,
4049
     *                       <b>JSON_HEX_TAG</b>,
4050
     *                       <b>JSON_HEX_AMP</b>,
4051
     *                       <b>JSON_HEX_APOS</b>,
4052
     *                       <b>JSON_NUMERIC_CHECK</b>,
4053
     *                       <b>JSON_PRETTY_PRINT</b>,
4054
     *                       <b>JSON_UNESCAPED_SLASHES</b>,
4055
     *                       <b>JSON_FORCE_OBJECT</b>,
4056
     *                       <b>JSON_UNESCAPED_UNICODE</b>. The behaviour of these
4057
     *                       constants is described on
4058
     *                       the JSON constants page.
4059
     *                       </p>
4060
     * @param int   $depth   [optional] <p>
4061
     *                       Set the maximum depth. Must be greater than zero.
4062
     *                       </p>
4063
     *
4064
     * @psalm-pure
4065
     *
4066
     * @return false|string
4067
     *                      A JSON encoded <strong>string</strong> on success or<br>
4068
     *                      <strong>FALSE</strong> on failure
4069
     */
4070 5
    public static function json_encode($value, int $options = 0, int $depth = 512)
4071
    {
4072 5
        $value = self::filter($value);
4073
4074 5
        if (self::$SUPPORT['json'] === false) {
4075
            throw new \RuntimeException('ext-json: is not installed');
4076
        }
4077
4078
        /** @noinspection PhpComposerExtensionStubsInspection */
4079 5
        return \json_encode($value, $options, $depth);
4080
    }
4081
4082
    /**
4083
     * Checks whether JSON is available on the server.
4084
     *
4085
     * @psalm-pure
4086
     *
4087
     * @return bool
4088
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
4089
     */
4090
    public static function json_loaded(): bool
4091
    {
4092
        return \function_exists('json_decode');
4093
    }
4094
4095
    /**
4096
     * Makes string's first char lowercase.
4097
     *
4098
     * @param string      $str                           <p>The input string</p>
4099
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
4100
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4101
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
4102
     *                                                   tr</p>
4103
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
4104
     *                                                   -> ß</p>
4105
     *
4106
     * @psalm-pure
4107
     *
4108
     * @return string the resulting string
4109
     */
4110 46
    public static function lcfirst(
4111
        string $str,
4112
        string $encoding = 'UTF-8',
4113
        bool $clean_utf8 = false,
4114
        string $lang = null,
4115
        bool $try_to_keep_the_string_length = false
4116
    ): string {
4117 46
        if ($clean_utf8 === true) {
4118
            $str = self::clean($str);
4119
        }
4120
4121 46
        $use_mb_functions = ($lang === null && $try_to_keep_the_string_length === false);
4122
4123 46
        if ($encoding === 'UTF-8') {
4124 43
            $str_part_two = (string) \mb_substr($str, 1);
4125
4126 43
            if ($use_mb_functions === true) {
4127 43
                $str_part_one = \mb_strtolower(
4128 43
                    (string) \mb_substr($str, 0, 1)
4129
                );
4130
            } else {
4131
                $str_part_one = self::strtolower(
4132
                    (string) \mb_substr($str, 0, 1),
4133
                    $encoding,
4134
                    false,
4135
                    $lang,
4136 43
                    $try_to_keep_the_string_length
4137
                );
4138
            }
4139
        } else {
4140 3
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4141
4142 3
            $str_part_two = (string) self::substr($str, 1, null, $encoding);
4143
4144 3
            $str_part_one = self::strtolower(
4145 3
                (string) self::substr($str, 0, 1, $encoding),
4146 3
                $encoding,
4147 3
                false,
4148 3
                $lang,
4149 3
                $try_to_keep_the_string_length
4150
            );
4151
        }
4152
4153 46
        return $str_part_one . $str_part_two;
4154
    }
4155
4156
    /**
4157
     * alias for "UTF8::lcfirst()"
4158
     *
4159
     * @param string      $str
4160
     * @param string      $encoding
4161
     * @param bool        $clean_utf8
4162
     * @param string|null $lang
4163
     * @param bool        $try_to_keep_the_string_length
4164
     *
4165
     * @psalm-pure
4166
     *
4167
     * @return string
4168
     *
4169
     * @see        UTF8::lcfirst()
4170
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
4171
     */
4172 2
    public static function lcword(
4173
        string $str,
4174
        string $encoding = 'UTF-8',
4175
        bool $clean_utf8 = false,
4176
        string $lang = null,
4177
        bool $try_to_keep_the_string_length = false
4178
    ): string {
4179 2
        return self::lcfirst(
4180 2
            $str,
4181 2
            $encoding,
4182 2
            $clean_utf8,
4183 2
            $lang,
4184 2
            $try_to_keep_the_string_length
4185
        );
4186
    }
4187
4188
    /**
4189
     * Lowercase for all words in the string.
4190
     *
4191
     * @param string      $str                           <p>The input string.</p>
4192
     * @param string[]    $exceptions                    [optional] <p>Exclusion for some words.</p>
4193
     * @param string      $char_list                     [optional] <p>Additional chars that contains to words and do
4194
     *                                                   not start a new word.</p>
4195
     * @param string      $encoding                      [optional] <p>Set the charset.</p>
4196
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
4197
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
4198
     *                                                   tr</p>
4199
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
4200
     *                                                   -> ß</p>
4201
     *
4202
     * @psalm-pure
4203
     *
4204
     * @return string
4205
     */
4206 2
    public static function lcwords(
4207
        string $str,
4208
        array $exceptions = [],
4209
        string $char_list = '',
4210
        string $encoding = 'UTF-8',
4211
        bool $clean_utf8 = false,
4212
        string $lang = null,
4213
        bool $try_to_keep_the_string_length = false
4214
    ): string {
4215 2
        if (!$str) {
4216 2
            return '';
4217
        }
4218
4219 2
        $words = self::str_to_words($str, $char_list);
4220 2
        $use_exceptions = $exceptions !== [];
4221
4222 2
        $words_str = '';
4223 2
        foreach ($words as &$word) {
4224 2
            if (!$word) {
4225 2
                continue;
4226
            }
4227
4228
            if (
4229 2
                $use_exceptions === false
4230
                ||
4231 2
                !\in_array($word, $exceptions, true)
4232
            ) {
4233 2
                $words_str .= self::lcfirst($word, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
4234
            } else {
4235 2
                $words_str .= $word;
4236
            }
4237
        }
4238
4239 2
        return $words_str;
4240
    }
4241
4242
    /**
4243
     * alias for "UTF8::lcfirst()"
4244
     *
4245
     * @param string      $str
4246
     * @param string      $encoding
4247
     * @param bool        $clean_utf8
4248
     * @param string|null $lang
4249
     * @param bool        $try_to_keep_the_string_length
4250
     *
4251
     * @psalm-pure
4252
     *
4253
     * @return string
4254
     *
4255
     * @see        UTF8::lcfirst()
4256
     * @deprecated <p>please use "UTF8::lcfirst()"</p>
4257
     */
4258 5
    public static function lowerCaseFirst(
4259
        string $str,
4260
        string $encoding = 'UTF-8',
4261
        bool $clean_utf8 = false,
4262
        string $lang = null,
4263
        bool $try_to_keep_the_string_length = false
4264
    ): string {
4265 5
        return self::lcfirst(
4266 5
            $str,
4267 5
            $encoding,
4268 5
            $clean_utf8,
4269 5
            $lang,
4270 5
            $try_to_keep_the_string_length
4271
        );
4272
    }
4273
4274
    /**
4275
     * Strip whitespace or other characters from the beginning of a UTF-8 string.
4276
     *
4277
     * @param string      $str   <p>The string to be trimmed</p>
4278
     * @param string|null $chars <p>Optional characters to be stripped</p>
4279
     *
4280
     * @psalm-pure
4281
     *
4282
     * @return string the string with unwanted characters stripped from the left
4283
     */
4284 22
    public static function ltrim(string $str = '', string $chars = null): string
4285
    {
4286 22
        if ($str === '') {
4287 3
            return '';
4288
        }
4289
4290 21
        if (self::$SUPPORT['mbstring'] === true) {
4291 21
            if ($chars) {
4292
                /** @noinspection PregQuoteUsageInspection */
4293 10
                $chars = \preg_quote($chars);
4294 10
                $pattern = "^[${chars}]+";
4295
            } else {
4296 14
                $pattern = '^[\\s]+';
4297
            }
4298
4299
            /** @noinspection PhpComposerExtensionStubsInspection */
4300 21
            return (string) \mb_ereg_replace($pattern, '', $str);
4301
        }
4302
4303
        if ($chars) {
4304
            $chars = \preg_quote($chars, '/');
4305
            $pattern = "^[${chars}]+";
4306
        } else {
4307
            $pattern = '^[\\s]+';
4308
        }
4309
4310
        return self::regex_replace($str, $pattern, '', '', '/');
4311
    }
4312
4313
    /**
4314
     * Returns the UTF-8 character with the maximum code point in the given data.
4315
     *
4316
     * @param array<string>|string $arg <p>A UTF-8 encoded string or an array of such strings.</p>
4317
     *
4318
     * @psalm-pure
4319
     *
4320
     * @return string|null the character with the highest code point than others, returns null on failure or empty input
4321
     */
4322 2
    public static function max($arg)
4323
    {
4324 2
        if (\is_array($arg) === true) {
4325 2
            $arg = \implode('', $arg);
4326
        }
4327
4328 2
        $codepoints = self::codepoints($arg, false);
4329 2
        if ($codepoints === []) {
4330 2
            return null;
4331
        }
4332
4333 2
        $codepoint_max = \max($codepoints);
4334
4335 2
        return self::chr($codepoint_max);
4336
    }
4337
4338
    /**
4339
     * Calculates and returns the maximum number of bytes taken by any
4340
     * UTF-8 encoded character in the given string.
4341
     *
4342
     * @param string $str <p>The original Unicode string.</p>
4343
     *
4344
     * @psalm-pure
4345
     *
4346
     * @return int
4347
     *             <p>Max byte lengths of the given chars.</p>
4348
     */
4349 2
    public static function max_chr_width(string $str): int
4350
    {
4351 2
        $bytes = self::chr_size_list($str);
4352 2
        if ($bytes !== []) {
4353 2
            return (int) \max($bytes);
4354
        }
4355
4356 2
        return 0;
4357
    }
4358
4359
    /**
4360
     * Checks whether mbstring is available on the server.
4361
     *
4362
     * @psalm-pure
4363
     *
4364
     * @return bool
4365
     *              <strong>true</strong> if available, <strong>false</strong> otherwise
4366
     */
4367 26
    public static function mbstring_loaded(): bool
4368
    {
4369 26
        return \extension_loaded('mbstring');
4370
    }
4371
4372
    /**
4373
     * Returns the UTF-8 character with the minimum code point in the given data.
4374
     *
4375
     * @param mixed $arg <strong>A UTF-8 encoded string or an array of such strings.</strong>
4376
     *
4377
     * @psalm-pure
4378
     *
4379
     * @return string|null the character with the lowest code point than others, returns null on failure or empty input
4380
     */
4381 2
    public static function min($arg)
4382
    {
4383 2
        if (\is_array($arg) === true) {
4384 2
            $arg = \implode('', $arg);
4385
        }
4386
4387 2
        $codepoints = self::codepoints($arg, false);
4388 2
        if ($codepoints === []) {
4389 2
            return null;
4390
        }
4391
4392 2
        $codepoint_min = \min($codepoints);
4393
4394 2
        return self::chr($codepoint_min);
4395
    }
4396
4397
    /**
4398
     * alias for "UTF8::normalize_encoding()"
4399
     *
4400
     * @param mixed $encoding
4401
     * @param mixed $fallback
4402
     *
4403
     * @psalm-pure
4404
     *
4405
     * @return mixed
4406
     *
4407
     * @see        UTF8::normalize_encoding()
4408
     * @deprecated <p>please use "UTF8::normalize_encoding()"</p>
4409
     */
4410 2
    public static function normalizeEncoding($encoding, $fallback = '')
4411
    {
4412 2
        return self::normalize_encoding($encoding, $fallback);
4413
    }
4414
4415
    /**
4416
     * Normalize the encoding-"name" input.
4417
     *
4418
     * @param mixed $encoding <p>e.g.: ISO, UTF8, WINDOWS-1251 etc.</p>
4419
     * @param mixed $fallback <p>e.g.: UTF-8</p>
4420
     *
4421
     * @psalm-pure
4422
     *
4423
     * @return mixed e.g.: ISO-8859-1, UTF-8, WINDOWS-1251 etc.<br>Will return a empty string as fallback (by default)
4424
     */
4425 331
    public static function normalize_encoding($encoding, $fallback = '')
4426
    {
4427
        /**
4428
         * @psalm-suppress ImpureStaticVariable
4429
         *
4430
         * @var array<string,string>
4431
         */
4432 331
        static $STATIC_NORMALIZE_ENCODING_CACHE = [];
4433
4434
        // init
4435 331
        $encoding = (string) $encoding;
4436
4437 331
        if (!$encoding) {
4438 285
            return $fallback;
4439
        }
4440
4441
        if (
4442 51
            $encoding === 'UTF-8'
4443
            ||
4444 51
            $encoding === 'UTF8'
4445
        ) {
4446 28
            return 'UTF-8';
4447
        }
4448
4449
        if (
4450 43
            $encoding === '8BIT'
4451
            ||
4452 43
            $encoding === 'BINARY'
4453
        ) {
4454
            return 'CP850';
4455
        }
4456
4457
        if (
4458 43
            $encoding === 'HTML'
4459
            ||
4460 43
            $encoding === 'HTML-ENTITIES'
4461
        ) {
4462 2
            return 'HTML-ENTITIES';
4463
        }
4464
4465
        if (
4466 43
            $encoding === 'ISO'
4467
            ||
4468 43
            $encoding === 'ISO-8859-1'
4469
        ) {
4470 39
            return 'ISO-8859-1';
4471
        }
4472
4473
        if (
4474 12
            $encoding === '1' // only a fallback, for non "strict_types" usage ...
4475
            ||
4476 12
            $encoding === '0' // only a fallback, for non "strict_types" usage ...
4477
        ) {
4478 1
            return $fallback;
4479
        }
4480
4481 11
        if (isset($STATIC_NORMALIZE_ENCODING_CACHE[$encoding])) {
4482 8
            return $STATIC_NORMALIZE_ENCODING_CACHE[$encoding];
4483
        }
4484
4485 5
        if (self::$ENCODINGS === null) {
4486 1
            self::$ENCODINGS = self::getData('encodings');
4487
        }
4488
4489 5
        if (\in_array($encoding, self::$ENCODINGS, true)) {
4490 3
            $STATIC_NORMALIZE_ENCODING_CACHE[$encoding] = $encoding;
4491
4492 3
            return $encoding;
4493
        }
4494
4495 4
        $encoding_original = $encoding;
4496 4
        $encoding = \strtoupper($encoding);
4497 4
        $encoding_upper_helper = (string) \preg_replace('/[^a-zA-Z0-9]/u', '', $encoding);
4498
4499
        $equivalences = [
4500 4
            'ISO8859'     => 'ISO-8859-1',
4501
            'ISO88591'    => 'ISO-8859-1',
4502
            'ISO'         => 'ISO-8859-1',
4503
            'LATIN'       => 'ISO-8859-1',
4504
            'LATIN1'      => 'ISO-8859-1', // Western European
4505
            'ISO88592'    => 'ISO-8859-2',
4506
            'LATIN2'      => 'ISO-8859-2', // Central European
4507
            'ISO88593'    => 'ISO-8859-3',
4508
            'LATIN3'      => 'ISO-8859-3', // Southern European
4509
            'ISO88594'    => 'ISO-8859-4',
4510
            'LATIN4'      => 'ISO-8859-4', // Northern European
4511
            'ISO88595'    => 'ISO-8859-5',
4512
            'ISO88596'    => 'ISO-8859-6', // Greek
4513
            'ISO88597'    => 'ISO-8859-7',
4514
            'ISO88598'    => 'ISO-8859-8', // Hebrew
4515
            'ISO88599'    => 'ISO-8859-9',
4516
            'LATIN5'      => 'ISO-8859-9', // Turkish
4517
            'ISO885911'   => 'ISO-8859-11',
4518
            'TIS620'      => 'ISO-8859-11', // Thai
4519
            'ISO885910'   => 'ISO-8859-10',
4520
            'LATIN6'      => 'ISO-8859-10', // Nordic
4521
            'ISO885913'   => 'ISO-8859-13',
4522
            'LATIN7'      => 'ISO-8859-13', // Baltic
4523
            'ISO885914'   => 'ISO-8859-14',
4524
            'LATIN8'      => 'ISO-8859-14', // Celtic
4525
            'ISO885915'   => 'ISO-8859-15',
4526
            'LATIN9'      => 'ISO-8859-15', // Western European (with some extra chars e.g. €)
4527
            'ISO885916'   => 'ISO-8859-16',
4528
            'LATIN10'     => 'ISO-8859-16', // Southeast European
4529
            'CP1250'      => 'WINDOWS-1250',
4530
            'WIN1250'     => 'WINDOWS-1250',
4531
            'WINDOWS1250' => 'WINDOWS-1250',
4532
            'CP1251'      => 'WINDOWS-1251',
4533
            'WIN1251'     => 'WINDOWS-1251',
4534
            'WINDOWS1251' => 'WINDOWS-1251',
4535
            'CP1252'      => 'WINDOWS-1252',
4536
            'WIN1252'     => 'WINDOWS-1252',
4537
            'WINDOWS1252' => 'WINDOWS-1252',
4538
            'CP1253'      => 'WINDOWS-1253',
4539
            'WIN1253'     => 'WINDOWS-1253',
4540
            'WINDOWS1253' => 'WINDOWS-1253',
4541
            'CP1254'      => 'WINDOWS-1254',
4542
            'WIN1254'     => 'WINDOWS-1254',
4543
            'WINDOWS1254' => 'WINDOWS-1254',
4544
            'CP1255'      => 'WINDOWS-1255',
4545
            'WIN1255'     => 'WINDOWS-1255',
4546
            'WINDOWS1255' => 'WINDOWS-1255',
4547
            'CP1256'      => 'WINDOWS-1256',
4548
            'WIN1256'     => 'WINDOWS-1256',
4549
            'WINDOWS1256' => 'WINDOWS-1256',
4550
            'CP1257'      => 'WINDOWS-1257',
4551
            'WIN1257'     => 'WINDOWS-1257',
4552
            'WINDOWS1257' => 'WINDOWS-1257',
4553
            'CP1258'      => 'WINDOWS-1258',
4554
            'WIN1258'     => 'WINDOWS-1258',
4555
            'WINDOWS1258' => 'WINDOWS-1258',
4556
            'UTF16'       => 'UTF-16',
4557
            'UTF32'       => 'UTF-32',
4558
            'UTF8'        => 'UTF-8',
4559
            'UTF'         => 'UTF-8',
4560
            'UTF7'        => 'UTF-7',
4561
            '8BIT'        => 'CP850',
4562
            'BINARY'      => 'CP850',
4563
        ];
4564
4565 4
        if (!empty($equivalences[$encoding_upper_helper])) {
4566 3
            $encoding = $equivalences[$encoding_upper_helper];
4567
        }
4568
4569 4
        $STATIC_NORMALIZE_ENCODING_CACHE[$encoding_original] = $encoding;
4570
4571 4
        return $encoding;
4572
    }
4573
4574
    /**
4575
     * Standardize line ending to unix-like.
4576
     *
4577
     * @param string $str      <p>The input string.</p>
4578
     * @param string $replacer <p>The replacer char e.g. "\n" (Linux) or "\r\n" (Windows). You can also use \PHP_EOL
4579
     *                         here.</p>
4580
     *
4581
     * @psalm-pure
4582
     *
4583
     * @return string
4584
     *                <p>A string with normalized line ending.</p>
4585
     */
4586 5
    public static function normalize_line_ending(string $str, $replacer = "\n"): string
4587
    {
4588 5
        return \str_replace(["\r\n", "\r", "\n"], $replacer, $str);
4589
    }
4590
4591
    /**
4592
     * Normalize some MS Word special characters.
4593
     *
4594
     * @param string $str <p>The string to be normalized.</p>
4595
     *
4596
     * @psalm-pure
4597
     *
4598
     * @return string
4599
     *                <p>A string with normalized characters for commonly used chars in Word documents.</p>
4600
     */
4601 10
    public static function normalize_msword(string $str): string
4602
    {
4603 10
        return ASCII::normalize_msword($str);
4604
    }
4605
4606
    /**
4607
     * Normalize the whitespace.
4608
     *
4609
     * @param string $str                        <p>The string to be normalized.</p>
4610
     * @param bool   $keep_non_breaking_space    [optional] <p>Set to true, to keep non-breaking-spaces.</p>
4611
     * @param bool   $keep_bidi_unicode_controls [optional] <p>Set to true, to keep non-printable (for the web)
4612
     *                                           bidirectional text chars.</p>
4613
     *
4614
     * @psalm-pure
4615
     *
4616
     * @return string
4617
     *                <p>A string with normalized whitespace.</p>
4618
     */
4619 61
    public static function normalize_whitespace(
4620
        string $str,
4621
        bool $keep_non_breaking_space = false,
4622
        bool $keep_bidi_unicode_controls = false
4623
    ): string {
4624 61
        return ASCII::normalize_whitespace(
4625 61
            $str,
4626 61
            $keep_non_breaking_space,
4627 61
            $keep_bidi_unicode_controls
4628
        );
4629
    }
4630
4631
    /**
4632
     * Calculates Unicode code point of the given UTF-8 encoded character.
4633
     *
4634
     * INFO: opposite to UTF8::chr()
4635
     *
4636
     * @param string $chr      <p>The character of which to calculate code point.<p/>
4637
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
4638
     *
4639
     * @psalm-pure
4640
     *
4641
     * @return int
4642
     *             <p>Unicode code point of the given character,<br>
4643
     *             0 on invalid UTF-8 byte sequence</p>
4644
     */
4645 26
    public static function ord($chr, string $encoding = 'UTF-8'): int
4646
    {
4647
        /**
4648
         * @psalm-suppress ImpureStaticVariable
4649
         *
4650
         * @var array<string,int>
4651
         */
4652 26
        static $CHAR_CACHE = [];
4653
4654
        // init
4655 26
        $chr = (string) $chr;
4656
4657 26
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
4658 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
4659
        }
4660
4661 26
        $cache_key = $chr . '_' . $encoding;
4662 26
        if (isset($CHAR_CACHE[$cache_key]) === true) {
4663 26
            return $CHAR_CACHE[$cache_key];
4664
        }
4665
4666
        // check again, if it's still not UTF-8
4667 10
        if ($encoding !== 'UTF-8') {
4668 3
            $chr = self::encode($encoding, $chr);
4669
        }
4670
4671 10
        if (self::$ORD === null) {
4672
            self::$ORD = self::getData('ord');
4673
        }
4674
4675 10
        if (isset(self::$ORD[$chr])) {
4676 10
            return $CHAR_CACHE[$cache_key] = self::$ORD[$chr];
4677
        }
4678
4679
        //
4680
        // fallback via "IntlChar"
4681
        //
4682
4683 6
        if (self::$SUPPORT['intlChar'] === true) {
4684
            /** @noinspection PhpComposerExtensionStubsInspection */
4685 5
            $code = \IntlChar::ord($chr);
4686 5
            if ($code) {
4687 5
                return $CHAR_CACHE[$cache_key] = $code;
4688
            }
4689
        }
4690
4691
        //
4692
        // fallback via vanilla php
4693
        //
4694
4695
        /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
4696 1
        $chr = \unpack('C*', (string) \substr($chr, 0, 4));
4697
        /** @noinspection OffsetOperationsInspection */
4698 1
        $code = $chr ? $chr[1] : 0;
4699
4700
        /** @noinspection OffsetOperationsInspection */
4701 1
        if ($code >= 0xF0 && isset($chr[4])) {
4702
            /** @noinspection UnnecessaryCastingInspection */
4703
            /** @noinspection OffsetOperationsInspection */
4704
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xF0) << 18) + (($chr[2] - 0x80) << 12) + (($chr[3] - 0x80) << 6) + $chr[4] - 0x80);
4705
        }
4706
4707
        /** @noinspection OffsetOperationsInspection */
4708 1
        if ($code >= 0xE0 && isset($chr[3])) {
4709
            /** @noinspection UnnecessaryCastingInspection */
4710
            /** @noinspection OffsetOperationsInspection */
4711 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xE0) << 12) + (($chr[2] - 0x80) << 6) + $chr[3] - 0x80);
4712
        }
4713
4714
        /** @noinspection OffsetOperationsInspection */
4715 1
        if ($code >= 0xC0 && isset($chr[2])) {
4716
            /** @noinspection UnnecessaryCastingInspection */
4717
            /** @noinspection OffsetOperationsInspection */
4718 1
            return $CHAR_CACHE[$cache_key] = (int) ((($code - 0xC0) << 6) + $chr[2] - 0x80);
4719
        }
4720
4721
        return $CHAR_CACHE[$cache_key] = $code;
4722
    }
4723
4724
    /**
4725
     * Parses the string into an array (into the the second parameter).
4726
     *
4727
     * WARNING: Unlike "parse_str()", this method does not (re-)place variables in the current scope,
4728
     *          if the second parameter is not set!
4729
     *
4730
     * @see http://php.net/manual/en/function.parse-str.php
4731
     *
4732
     * @param string $str        <p>The input string.</p>
4733
     * @param array  $result     <p>The result will be returned into this reference parameter.</p>
4734
     * @param bool   $clean_utf8 [optional] <p>Remove non UTF-8 chars from the string.</p>
4735
     *
4736
     * @psalm-pure
4737
     *
4738
     * @return bool
4739
     *              <p>Will return <strong>false</strong> if php can't parse the string and we haven't any $result.</p>
4740
     */
4741 2
    public static function parse_str(string $str, &$result, bool $clean_utf8 = false): bool
4742
    {
4743 2
        if ($clean_utf8 === true) {
4744 2
            $str = self::clean($str);
4745
        }
4746
4747 2
        if (self::$SUPPORT['mbstring'] === true) {
4748 2
            $return = \mb_parse_str($str, $result);
4749
4750 2
            return $return !== false && $result !== [];
4751
        }
4752
4753
        /**
4754
         * @psalm-suppress ImpureFunctionCall - we use the second parameter, so we don't change variables by magic
4755
         */
4756
        \parse_str($str, $result);
4757
4758
        return $result !== [];
4759
    }
4760
4761
    /**
4762
     * Checks if \u modifier is available that enables Unicode support in PCRE.
4763
     *
4764
     * @psalm-pure
4765
     *
4766
     * @return bool
4767
     *              <p>
4768
     *              <strong>true</strong> if support is available,<br>
4769
     *              <strong>false</strong> otherwise
4770
     *              </p>
4771
     */
4772 102
    public static function pcre_utf8_support(): bool
4773
    {
4774
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
4775 102
        return (bool) @\preg_match('//u', '');
4776
    }
4777
4778
    /**
4779
     * Create an array containing a range of UTF-8 characters.
4780
     *
4781
     * @param mixed     $var1      <p>Numeric or hexadecimal code points, or a UTF-8 character to start from.</p>
4782
     * @param mixed     $var2      <p>Numeric or hexadecimal code points, or a UTF-8 character to end at.</p>
4783
     * @param bool      $use_ctype <p>use ctype to detect numeric and hexadecimal, otherwise we will use a simple
4784
     *                             "is_numeric"</p>
4785
     * @param string    $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
4786
     * @param float|int $step      [optional] <p>
4787
     *                             If a step value is given, it will be used as the
4788
     *                             increment between elements in the sequence. step
4789
     *                             should be given as a positive number. If not specified,
4790
     *                             step will default to 1.
4791
     *                             </p>
4792
     *
4793
     * @psalm-pure
4794
     *
4795
     * @return string[]
4796
     */
4797 2
    public static function range(
4798
        $var1,
4799
        $var2,
4800
        bool $use_ctype = true,
4801
        string $encoding = 'UTF-8',
4802
        $step = 1
4803
    ): array {
4804 2
        if (!$var1 || !$var2) {
4805 2
            return [];
4806
        }
4807
4808 2
        if ($step !== 1) {
4809
            /**
4810
             * @psalm-suppress RedundantConditionGivenDocblockType
4811
             * @psalm-suppress DocblockTypeContradiction
4812
             */
4813 1
            if (!\is_numeric($step)) {
0 ignored issues
show
introduced by
The condition is_numeric($step) is always true.
Loading history...
4814
                throw new \InvalidArgumentException('$step need to be a number, type given: ' . \gettype($step));
4815
            }
4816
4817
            /**
4818
             * @psalm-suppress RedundantConditionGivenDocblockType - false-positive from psalm?
4819
             */
4820 1
            if ($step <= 0) {
4821
                throw new \InvalidArgumentException('$step need to be a positive number, given: ' . $step);
4822
            }
4823
        }
4824
4825 2
        if ($use_ctype && self::$SUPPORT['ctype'] === false) {
4826
            throw new \RuntimeException('ext-ctype: is not installed');
4827
        }
4828
4829 2
        $is_digit = false;
4830 2
        $is_xdigit = false;
4831
4832
        /** @noinspection PhpComposerExtensionStubsInspection */
4833 2
        if ($use_ctype && \ctype_digit((string) $var1) && \ctype_digit((string) $var2)) {
4834 2
            $is_digit = true;
4835 2
            $start = (int) $var1;
4836 2
        } /** @noinspection PhpComposerExtensionStubsInspection */ elseif ($use_ctype && \ctype_xdigit($var1) && \ctype_xdigit($var2)) {
4837
            $is_xdigit = true;
4838
            $start = (int) self::hex_to_int($var1);
4839 2
        } elseif (!$use_ctype && \is_numeric($var1)) {
4840 1
            $start = (int) $var1;
4841
        } else {
4842 2
            $start = self::ord($var1);
4843
        }
4844
4845 2
        if (!$start) {
4846
            return [];
4847
        }
4848
4849 2
        if ($is_digit) {
4850 2
            $end = (int) $var2;
4851 2
        } elseif ($is_xdigit) {
4852
            $end = (int) self::hex_to_int($var2);
4853 2
        } elseif (!$use_ctype && \is_numeric($var2)) {
4854 1
            $end = (int) $var2;
4855
        } else {
4856 2
            $end = self::ord($var2);
4857
        }
4858
4859 2
        if (!$end) {
4860
            return [];
4861
        }
4862
4863 2
        $array = [];
4864 2
        foreach (\range($start, $end, $step) as $i) {
4865 2
            $array[] = (string) self::chr((int) $i, $encoding);
4866
        }
4867
4868 2
        return $array;
4869
    }
4870
4871
    /**
4872
     * Multi decode HTML entity + fix urlencoded-win1252-chars.
4873
     *
4874
     * e.g:
4875
     * 'test+test'                     => 'test+test'
4876
     * 'D&#252;sseldorf'               => 'Düsseldorf'
4877
     * 'D%FCsseldorf'                  => 'Düsseldorf'
4878
     * 'D&#xFC;sseldorf'               => 'Düsseldorf'
4879
     * 'D%26%23xFC%3Bsseldorf'         => 'Düsseldorf'
4880
     * 'Düsseldorf'                   => 'Düsseldorf'
4881
     * 'D%C3%BCsseldorf'               => 'Düsseldorf'
4882
     * 'D%C3%83%C2%BCsseldorf'         => 'Düsseldorf'
4883
     * 'D%25C3%2583%25C2%25BCsseldorf' => 'Düsseldorf'
4884
     *
4885
     * @param string $str          <p>The input string.</p>
4886
     * @param bool   $multi_decode <p>Decode as often as possible.</p>
4887
     *
4888
     * @psalm-pure
4889
     *
4890
     * @return string
4891
     *                <p>The decoded URL, as a string.</p>
4892
     */
4893 7
    public static function rawurldecode(string $str, bool $multi_decode = true): string
4894
    {
4895 7
        if ($str === '') {
4896 4
            return '';
4897
        }
4898
4899
        if (
4900 7
            \strpos($str, '&') === false
4901
            &&
4902 7
            \strpos($str, '%') === false
4903
            &&
4904 7
            \strpos($str, '+') === false
4905
            &&
4906 7
            \strpos($str, '\u') === false
4907
        ) {
4908 4
            return self::fix_simple_utf8($str);
4909
        }
4910
4911 7
        $str = self::urldecode_unicode_helper($str);
4912
4913 7
        if ($multi_decode) {
4914
            do {
4915 6
                $str_compare = $str;
4916
4917
                /**
4918
                 * @psalm-suppress PossiblyInvalidArgument
4919
                 */
4920 6
                $str = self::fix_simple_utf8(
4921 6
                    \rawurldecode(
4922 6
                        self::html_entity_decode(
4923 6
                            self::to_utf8($str),
4924 6
                            \ENT_QUOTES | \ENT_HTML5
4925
                        )
4926
                    )
4927
                );
4928 6
            } while ($str_compare !== $str);
4929
        } else {
4930
            /**
4931
             * @psalm-suppress PossiblyInvalidArgument
4932
             */
4933 1
            $str = self::fix_simple_utf8(
4934 1
                \rawurldecode(
4935 1
                    self::html_entity_decode(
4936 1
                        self::to_utf8($str),
4937 1
                        \ENT_QUOTES | \ENT_HTML5
4938
                    )
4939
                )
4940
            );
4941
        }
4942
4943 7
        return $str;
4944
    }
4945
4946
    /**
4947
     * Replaces all occurrences of $pattern in $str by $replacement.
4948
     *
4949
     * @param string $str         <p>The input string.</p>
4950
     * @param string $pattern     <p>The regular expression pattern.</p>
4951
     * @param string $replacement <p>The string to replace with.</p>
4952
     * @param string $options     [optional] <p>Matching conditions to be used.</p>
4953
     * @param string $delimiter   [optional] <p>Delimiter the the regex. Default: '/'</p>
4954
     *
4955
     * @psalm-pure
4956
     *
4957
     * @return string
4958
     */
4959 18
    public static function regex_replace(
4960
        string $str,
4961
        string $pattern,
4962
        string $replacement,
4963
        string $options = '',
4964
        string $delimiter = '/'
4965
    ): string {
4966 18
        if ($options === 'msr') {
4967 9
            $options = 'ms';
4968
        }
4969
4970
        // fallback
4971 18
        if (!$delimiter) {
4972
            $delimiter = '/';
4973
        }
4974
4975 18
        return (string) \preg_replace(
4976 18
            $delimiter . $pattern . $delimiter . 'u' . $options,
4977 18
            $replacement,
4978 18
            $str
4979
        );
4980
    }
4981
4982
    /**
4983
     * alias for "UTF8::remove_bom()"
4984
     *
4985
     * @param string $str
4986
     *
4987
     * @psalm-pure
4988
     *
4989
     * @return string
4990
     *
4991
     * @see        UTF8::remove_bom()
4992
     * @deprecated <p>please use "UTF8::remove_bom()"</p>
4993
     */
4994
    public static function removeBOM(string $str): string
4995
    {
4996
        return self::remove_bom($str);
4997
    }
4998
4999
    /**
5000
     * Remove the BOM from UTF-8 / UTF-16 / UTF-32 strings.
5001
     *
5002
     * @param string $str <p>The input string.</p>
5003
     *
5004
     * @psalm-pure
5005
     *
5006
     * @return string
5007
     *                <p>A string without UTF-BOM.</p>
5008
     */
5009 55
    public static function remove_bom(string $str): string
5010
    {
5011 55
        if ($str === '') {
5012 9
            return '';
5013
        }
5014
5015 55
        $str_length = \strlen($str);
5016 55
        foreach (self::$BOM as $bom_string => $bom_byte_length) {
5017 55
            if (\strpos($str, $bom_string, 0) === 0) {
5018
                /** @var false|string $str_tmp - needed for PhpStan (stubs error) */
5019 11
                $str_tmp = \substr($str, $bom_byte_length, $str_length);
5020 11
                if ($str_tmp === false) {
5021
                    return '';
5022
                }
5023
5024 11
                $str_length -= (int) $bom_byte_length;
5025
5026 55
                $str = (string) $str_tmp;
5027
            }
5028
        }
5029
5030 55
        return $str;
5031
    }
5032
5033
    /**
5034
     * Removes duplicate occurrences of a string in another string.
5035
     *
5036
     * @param string          $str  <p>The base string.</p>
5037
     * @param string|string[] $what <p>String to search for in the base string.</p>
5038
     *
5039
     * @psalm-pure
5040
     *
5041
     * @return string
5042
     *                <p>A string with removed duplicates.</p>
5043
     */
5044 2
    public static function remove_duplicates(string $str, $what = ' '): string
5045
    {
5046 2
        if (\is_string($what) === true) {
5047 2
            $what = [$what];
5048
        }
5049
5050
        /**
5051
         * @psalm-suppress RedundantConditionGivenDocblockType
5052
         */
5053 2
        if (\is_array($what) === true) {
0 ignored issues
show
introduced by
The condition is_array($what) === true is always true.
Loading history...
5054 2
            foreach ($what as $item) {
5055 2
                $str = (string) \preg_replace('/(' . \preg_quote($item, '/') . ')+/u', $item, $str);
5056
            }
5057
        }
5058
5059 2
        return $str;
5060
    }
5061
5062
    /**
5063
     * Remove html via "strip_tags()" from the string.
5064
     *
5065
     * @param string $str            <p>The input string.</p>
5066
     * @param string $allowable_tags [optional] <p>You can use the optional second parameter to specify tags which
5067
     *                               should not be stripped. Default: null
5068
     *                               </p>
5069
     *
5070
     * @psalm-pure
5071
     *
5072
     * @return string
5073
     *                <p>A string with without html tags.</p>
5074
     */
5075 6
    public static function remove_html(string $str, string $allowable_tags = ''): string
5076
    {
5077 6
        return \strip_tags($str, $allowable_tags);
5078
    }
5079
5080
    /**
5081
     * Remove all breaks [<br> | \r\n | \r | \n | ...] from the string.
5082
     *
5083
     * @param string $str         <p>The input string.</p>
5084
     * @param string $replacement [optional] <p>Default is a empty string.</p>
5085
     *
5086
     * @psalm-pure
5087
     *
5088
     * @return string
5089
     *                <p>A string without breaks.</p>
5090
     */
5091 6
    public static function remove_html_breaks(string $str, string $replacement = ''): string
5092
    {
5093 6
        return (string) \preg_replace("#/\r\n|\r|\n|<br.*/?>#isU", $replacement, $str);
5094
    }
5095
5096
    /**
5097
     * Remove invisible characters from a string.
5098
     *
5099
     * e.g.: This prevents sandwiching null characters between ascii characters, like Java\0script.
5100
     *
5101
     * copy&past from https://github.com/bcit-ci/CodeIgniter/blob/develop/system/core/Common.php
5102
     *
5103
     * @param string $str         <p>The input string.</p>
5104
     * @param bool   $url_encoded [optional] <p>
5105
     *                            Try to remove url encoded control character.
5106
     *                            WARNING: maybe contains false-positives e.g. aa%0Baa -> aaaa.
5107
     *                            <br>
5108
     *                            Default: false
5109
     *                            </p>
5110
     * @param string $replacement [optional] <p>The replacement character.</p>
5111
     *
5112
     * @psalm-pure
5113
     *
5114
     * @return string
5115
     *                <p>A string without invisible chars.</p>
5116
     */
5117 89
    public static function remove_invisible_characters(
5118
        string $str,
5119
        bool $url_encoded = false,
5120
        string $replacement = ''
5121
    ): string {
5122 89
        return ASCII::remove_invisible_characters(
5123 89
            $str,
5124 89
            $url_encoded,
5125 89
            $replacement
5126
        );
5127
    }
5128
5129
    /**
5130
     * Returns a new string with the prefix $substring removed, if present.
5131
     *
5132
     * @param string $str       <p>The input string.</p>
5133
     * @param string $substring <p>The prefix to remove.</p>
5134
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5135
     *
5136
     * @psalm-pure
5137
     *
5138
     * @return string
5139
     *                <p>A string without the prefix $substring.</p>
5140
     */
5141 12
    public static function remove_left(
5142
        string $str,
5143
        string $substring,
5144
        string $encoding = 'UTF-8'
5145
    ): string {
5146 12
        if ($substring && \strpos($str, $substring) === 0) {
5147 6
            if ($encoding === 'UTF-8') {
5148 4
                return (string) \mb_substr(
5149 4
                    $str,
5150 4
                    (int) \mb_strlen($substring)
5151
                );
5152
            }
5153
5154 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5155
5156 2
            return (string) self::substr(
5157 2
                $str,
5158 2
                (int) self::strlen($substring, $encoding),
5159 2
                null,
5160 2
                $encoding
5161
            );
5162
        }
5163
5164 6
        return $str;
5165
    }
5166
5167
    /**
5168
     * Returns a new string with the suffix $substring removed, if present.
5169
     *
5170
     * @param string $str
5171
     * @param string $substring <p>The suffix to remove.</p>
5172
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
5173
     *
5174
     * @psalm-pure
5175
     *
5176
     * @return string
5177
     *                <p>A string having a $str without the suffix $substring.</p>
5178
     */
5179 12
    public static function remove_right(
5180
        string $str,
5181
        string $substring,
5182
        string $encoding = 'UTF-8'
5183
    ): string {
5184 12
        if ($substring && \substr($str, -\strlen($substring)) === $substring) {
5185 6
            if ($encoding === 'UTF-8') {
5186 4
                return (string) \mb_substr(
5187 4
                    $str,
5188 4
                    0,
5189 4
                    (int) \mb_strlen($str) - (int) \mb_strlen($substring)
5190
                );
5191
            }
5192
5193 2
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5194
5195 2
            return (string) self::substr(
5196 2
                $str,
5197 2
                0,
5198 2
                (int) self::strlen($str, $encoding) - (int) self::strlen($substring, $encoding),
5199 2
                $encoding
5200
            );
5201
        }
5202
5203 6
        return $str;
5204
    }
5205
5206
    /**
5207
     * Replaces all occurrences of $search in $str by $replacement.
5208
     *
5209
     * @param string $str            <p>The input string.</p>
5210
     * @param string $search         <p>The needle to search for.</p>
5211
     * @param string $replacement    <p>The string to replace with.</p>
5212
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5213
     *
5214
     * @psalm-pure
5215
     *
5216
     * @return string
5217
     *                <p>A string with replaced parts.</p>
5218
     */
5219 29
    public static function replace(
5220
        string $str,
5221
        string $search,
5222
        string $replacement,
5223
        bool $case_sensitive = true
5224
    ): string {
5225 29
        if ($case_sensitive) {
5226 22
            return \str_replace($search, $replacement, $str);
5227
        }
5228
5229 7
        return self::str_ireplace($search, $replacement, $str);
5230
    }
5231
5232
    /**
5233
     * Replaces all occurrences of $search in $str by $replacement.
5234
     *
5235
     * @param string       $str            <p>The input string.</p>
5236
     * @param array        $search         <p>The elements to search for.</p>
5237
     * @param array|string $replacement    <p>The string to replace with.</p>
5238
     * @param bool         $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5239
     *
5240
     * @psalm-pure
5241
     *
5242
     * @return string
5243
     *                <p>A string with replaced parts.</p>
5244
     */
5245 30
    public static function replace_all(
5246
        string $str,
5247
        array $search,
5248
        $replacement,
5249
        bool $case_sensitive = true
5250
    ): string {
5251 30
        if ($case_sensitive) {
5252 23
            return \str_replace($search, $replacement, $str);
5253
        }
5254
5255 7
        return self::str_ireplace($search, $replacement, $str);
5256
    }
5257
5258
    /**
5259
     * Replace the diamond question mark (�) and invalid-UTF8 chars with the replacement.
5260
     *
5261
     * @param string $str                        <p>The input string</p>
5262
     * @param string $replacement_char           <p>The replacement character.</p>
5263
     * @param bool   $process_invalid_utf8_chars <p>Convert invalid UTF-8 chars </p>
5264
     *
5265
     * @psalm-pure
5266
     *
5267
     * @return string
5268
     *                <p>A string without diamond question marks (�).</p>
5269
     */
5270 35
    public static function replace_diamond_question_mark(
5271
        string $str,
5272
        string $replacement_char = '',
5273
        bool $process_invalid_utf8_chars = true
5274
    ): string {
5275 35
        if ($str === '') {
5276 9
            return '';
5277
        }
5278
5279 35
        if ($process_invalid_utf8_chars === true) {
5280 35
            $replacement_char_helper = $replacement_char;
5281 35
            if ($replacement_char === '') {
5282 35
                $replacement_char_helper = 'none';
5283
            }
5284
5285 35
            if (self::$SUPPORT['mbstring'] === false) {
5286
                // if there is no native support for "mbstring",
5287
                // then we need to clean the string before ...
5288
                $str = self::clean($str);
5289
            }
5290
5291
            /**
5292
             * @psalm-suppress ImpureFunctionCall - we will reset the value in the next step
5293
             */
5294 35
            $save = \mb_substitute_character();
5295 35
            \mb_substitute_character($replacement_char_helper);
5296
            // the polyfill maybe return false, so cast to string
5297 35
            $str = (string) \mb_convert_encoding($str, 'UTF-8', 'UTF-8');
5298 35
            \mb_substitute_character($save);
5299
        }
5300
5301 35
        return \str_replace(
5302
            [
5303 35
                "\xEF\xBF\xBD",
5304
                '�',
5305
            ],
5306
            [
5307 35
                $replacement_char,
5308 35
                $replacement_char,
5309
            ],
5310 35
            $str
5311
        );
5312
    }
5313
5314
    /**
5315
     * Strip whitespace or other characters from the end of a UTF-8 string.
5316
     *
5317
     * @param string      $str   <p>The string to be trimmed.</p>
5318
     * @param string|null $chars <p>Optional characters to be stripped.</p>
5319
     *
5320
     * @psalm-pure
5321
     *
5322
     * @return string
5323
     *                <p>A string with unwanted characters stripped from the right.</p>
5324
     */
5325 20
    public static function rtrim(string $str = '', string $chars = null): string
5326
    {
5327 20
        if ($str === '') {
5328 3
            return '';
5329
        }
5330
5331 19
        if (self::$SUPPORT['mbstring'] === true) {
5332 19
            if ($chars) {
5333
                /** @noinspection PregQuoteUsageInspection */
5334 8
                $chars = \preg_quote($chars);
5335 8
                $pattern = "[${chars}]+$";
5336
            } else {
5337 14
                $pattern = '[\\s]+$';
5338
            }
5339
5340
            /** @noinspection PhpComposerExtensionStubsInspection */
5341 19
            return (string) \mb_ereg_replace($pattern, '', $str);
5342
        }
5343
5344
        if ($chars) {
5345
            $chars = \preg_quote($chars, '/');
5346
            $pattern = "[${chars}]+$";
5347
        } else {
5348
            $pattern = '[\\s]+$';
5349
        }
5350
5351
        return self::regex_replace($str, $pattern, '', '', '/');
5352
    }
5353
5354
    /**
5355
     * WARNING: Print native UTF-8 support (libs) by default, e.g. for debugging.
5356
     *
5357
     * @param bool $useEcho
5358
     *
5359
     * @psalm-pure
5360
     *
5361
     * @return string|void
5362
     */
5363 2
    public static function showSupport(bool $useEcho = true)
5364
    {
5365
        // init
5366 2
        $html = '';
5367
5368 2
        $html .= '<pre>';
5369
        /** @noinspection AlterInForeachInspection */
5370 2
        foreach (self::$SUPPORT as $key => &$value) {
5371 2
            $html .= $key . ' - ' . \print_r($value, true) . "\n<br>";
5372
        }
5373 2
        $html .= '</pre>';
5374
5375 2
        if ($useEcho) {
5376 2
            echo $html;
5377
        }
5378
5379 2
        return $html;
5380
    }
5381
5382
    /**
5383
     * Converts a UTF-8 character to HTML Numbered Entity like "&#123;".
5384
     *
5385
     * @param string $char             <p>The Unicode character to be encoded as numbered entity.</p>
5386
     * @param bool   $keep_ascii_chars <p>Set to <strong>true</strong> to keep ASCII chars.</>
5387
     * @param string $encoding         [optional] <p>Set the charset for e.g. "mb_" function</p>
5388
     *
5389
     * @psalm-pure
5390
     *
5391
     * @return string
5392
     *                <p>The HTML numbered entity for the given character.</p>
5393
     */
5394 2
    public static function single_chr_html_encode(
5395
        string $char,
5396
        bool $keep_ascii_chars = false,
5397
        string $encoding = 'UTF-8'
5398
    ): string {
5399 2
        if ($char === '') {
5400 2
            return '';
5401
        }
5402
5403
        if (
5404 2
            $keep_ascii_chars === true
5405
            &&
5406 2
            ASCII::is_ascii($char) === true
5407
        ) {
5408 2
            return $char;
5409
        }
5410
5411 2
        return '&#' . self::ord($char, $encoding) . ';';
5412
    }
5413
5414
    /**
5415
     * @param string $str
5416
     * @param int    $tab_length
5417
     *
5418
     * @psalm-pure
5419
     *
5420
     * @return string
5421
     */
5422 5
    public static function spaces_to_tabs(string $str, int $tab_length = 4): string
5423
    {
5424 5
        if ($tab_length === 4) {
5425 3
            $tab = '    ';
5426 2
        } elseif ($tab_length === 2) {
5427 1
            $tab = '  ';
5428
        } else {
5429 1
            $tab = \str_repeat(' ', $tab_length);
5430
        }
5431
5432 5
        return \str_replace($tab, "\t", $str);
5433
    }
5434
5435
    /**
5436
     * alias for "UTF8::str_split()"
5437
     *
5438
     * @param string|string[] $str
5439
     * @param int             $length
5440
     * @param bool            $clean_utf8
5441
     *
5442
     * @psalm-pure
5443
     *
5444
     * @return string[]
5445
     *
5446
     * @see        UTF8::str_split()
5447
     * @deprecated <p>please use "UTF8::str_split()"</p>
5448
     */
5449 9
    public static function split(
5450
        $str,
5451
        int $length = 1,
5452
        bool $clean_utf8 = false
5453
    ): array {
5454 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...
5455
    }
5456
5457
    /**
5458
     * alias for "UTF8::str_starts_with()"
5459
     *
5460
     * @param string $haystack
5461
     * @param string $needle
5462
     *
5463
     * @psalm-pure
5464
     *
5465
     * @return bool
5466
     *
5467
     * @see        UTF8::str_starts_with()
5468
     * @deprecated <p>please use "UTF8::str_starts_with()"</p>
5469
     */
5470
    public static function str_begins(string $haystack, string $needle): bool
5471
    {
5472
        return self::str_starts_with($haystack, $needle);
5473
    }
5474
5475
    /**
5476
     * Returns a camelCase version of the string. Trims surrounding spaces,
5477
     * capitalizes letters following digits, spaces, dashes and underscores,
5478
     * and removes spaces, dashes, as well as underscores.
5479
     *
5480
     * @param string      $str                           <p>The input string.</p>
5481
     * @param string      $encoding                      [optional] <p>Default: 'UTF-8'</p>
5482
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5483
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5484
     *                                                   tr</p>
5485
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ
5486
     *                                                   -> ß</p>
5487
     *
5488
     * @psalm-pure
5489
     *
5490
     * @return string
5491
     */
5492 32
    public static function str_camelize(
5493
        string $str,
5494
        string $encoding = 'UTF-8',
5495
        bool $clean_utf8 = false,
5496
        string $lang = null,
5497
        bool $try_to_keep_the_string_length = false
5498
    ): string {
5499 32
        if ($clean_utf8 === true) {
5500
            $str = self::clean($str);
5501
        }
5502
5503 32
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
5504 26
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
5505
        }
5506
5507 32
        $str = self::lcfirst(
5508 32
            \trim($str),
5509 32
            $encoding,
5510 32
            false,
5511 32
            $lang,
5512 32
            $try_to_keep_the_string_length
5513
        );
5514 32
        $str = (string) \preg_replace('/^[-_]+/', '', $str);
5515
5516 32
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5517
5518 32
        $str = (string) \preg_replace_callback(
5519 32
            '/[-_\\s]+(.)?/u',
5520
            /**
5521
             * @param array $match
5522
             *
5523
             * @psalm-pure
5524
             *
5525
             * @return string
5526
             */
5527
            static function (array $match) use ($use_mb_functions, $encoding, $lang, $try_to_keep_the_string_length): string {
5528 27
                if (isset($match[1])) {
5529 27
                    if ($use_mb_functions === true) {
5530 27
                        if ($encoding === 'UTF-8') {
5531 27
                            return \mb_strtoupper($match[1]);
5532
                        }
5533
5534
                        return \mb_strtoupper($match[1], $encoding);
5535
                    }
5536
5537
                    return self::strtoupper($match[1], $encoding, false, $lang, $try_to_keep_the_string_length);
5538
                }
5539
5540 1
                return '';
5541 32
            },
5542 32
            $str
5543
        );
5544
5545 32
        return (string) \preg_replace_callback(
5546 32
            '/[\\p{N}]+(.)?/u',
5547
            /**
5548
             * @param array $match
5549
             *
5550
             * @psalm-pure
5551
             *
5552
             * @return string
5553
             */
5554
            static function (array $match) use ($use_mb_functions, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length): string {
5555 6
                if ($use_mb_functions === true) {
5556 6
                    if ($encoding === 'UTF-8') {
5557 6
                        return \mb_strtoupper($match[0]);
5558
                    }
5559
5560
                    return \mb_strtoupper($match[0], $encoding);
5561
                }
5562
5563
                return self::strtoupper($match[0], $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5564 32
            },
5565 32
            $str
5566
        );
5567
    }
5568
5569
    /**
5570
     * Returns the string with the first letter of each word capitalized,
5571
     * except for when the word is a name which shouldn't be capitalized.
5572
     *
5573
     * @param string $str
5574
     *
5575
     * @psalm-pure
5576
     *
5577
     * @return string
5578
     *                <p>A string with $str capitalized.</p>
5579
     */
5580 1
    public static function str_capitalize_name(string $str): string
5581
    {
5582 1
        return self::str_capitalize_name_helper(
5583 1
            self::str_capitalize_name_helper(
5584 1
                self::collapse_whitespace($str),
5585 1
                ' '
5586
            ),
5587 1
            '-'
5588
        );
5589
    }
5590
5591
    /**
5592
     * Returns true if the string contains $needle, false otherwise. By default
5593
     * the comparison is case-sensitive, but can be made insensitive by setting
5594
     * $case_sensitive to false.
5595
     *
5596
     * @param string $haystack       <p>The input string.</p>
5597
     * @param string $needle         <p>Substring to look for.</p>
5598
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5599
     *
5600
     * @psalm-pure
5601
     *
5602
     * @return bool whether or not $haystack contains $needle
5603
     */
5604 21
    public static function str_contains(
5605
        string $haystack,
5606
        string $needle,
5607
        bool $case_sensitive = true
5608
    ): bool {
5609 21
        if ($case_sensitive) {
5610 11
            return \strpos($haystack, $needle) !== false;
5611
        }
5612
5613 10
        return \mb_stripos($haystack, $needle) !== false;
5614
    }
5615
5616
    /**
5617
     * Returns true if the string contains all $needles, false otherwise. By
5618
     * default the comparison is case-sensitive, but can be made insensitive by
5619
     * setting $case_sensitive to false.
5620
     *
5621
     * @param string $haystack       <p>The input string.</p>
5622
     * @param array  $needles        <p>SubStrings to look for.</p>
5623
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5624
     *
5625
     * @psalm-pure
5626
     *
5627
     * @return bool whether or not $haystack contains $needle
5628
     */
5629 45
    public static function str_contains_all(
5630
        string $haystack,
5631
        array $needles,
5632
        bool $case_sensitive = true
5633
    ): bool {
5634 45
        if ($haystack === '' || $needles === []) {
5635 1
            return false;
5636
        }
5637
5638
        /** @noinspection LoopWhichDoesNotLoopInspection */
5639 44
        foreach ($needles as &$needle) {
5640 44
            if ($case_sensitive) {
5641
                /** @noinspection NestedPositiveIfStatementsInspection */
5642 24
                if (!$needle || \strpos($haystack, $needle) === false) {
5643 12
                    return false;
5644
                }
5645
            }
5646
5647 33
            if (!$needle || \mb_stripos($haystack, $needle) === false) {
5648 33
                return false;
5649
            }
5650
        }
5651
5652 24
        return true;
5653
    }
5654
5655
    /**
5656
     * Returns true if the string contains any $needles, false otherwise. By
5657
     * default the comparison is case-sensitive, but can be made insensitive by
5658
     * setting $case_sensitive to false.
5659
     *
5660
     * @param string $haystack       <p>The input string.</p>
5661
     * @param array  $needles        <p>SubStrings to look for.</p>
5662
     * @param bool   $case_sensitive [optional] <p>Whether or not to enforce case-sensitivity. Default: true</p>
5663
     *
5664
     * @psalm-pure
5665
     *
5666
     * @return bool
5667
     *              Whether or not $str contains $needle
5668
     */
5669 46
    public static function str_contains_any(
5670
        string $haystack,
5671
        array $needles,
5672
        bool $case_sensitive = true
5673
    ): bool {
5674 46
        if ($haystack === '' || $needles === []) {
5675 1
            return false;
5676
        }
5677
5678
        /** @noinspection LoopWhichDoesNotLoopInspection */
5679 45
        foreach ($needles as &$needle) {
5680 45
            if (!$needle) {
5681
                continue;
5682
            }
5683
5684 45
            if ($case_sensitive) {
5685 25
                if (\strpos($haystack, $needle) !== false) {
5686 14
                    return true;
5687
                }
5688
5689 13
                continue;
5690
            }
5691
5692 20
            if (\mb_stripos($haystack, $needle) !== false) {
5693 20
                return true;
5694
            }
5695
        }
5696
5697 19
        return false;
5698
    }
5699
5700
    /**
5701
     * Returns a lowercase and trimmed string separated by dashes. Dashes are
5702
     * inserted before uppercase characters (with the exception of the first
5703
     * character of the string), and in place of spaces as well as underscores.
5704
     *
5705
     * @param string $str      <p>The input string.</p>
5706
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
5707
     *
5708
     * @psalm-pure
5709
     *
5710
     * @return string
5711
     */
5712 19
    public static function str_dasherize(string $str, string $encoding = 'UTF-8'): string
5713
    {
5714 19
        return self::str_delimit($str, '-', $encoding);
5715
    }
5716
5717
    /**
5718
     * Returns a lowercase and trimmed string separated by the given delimiter.
5719
     * Delimiters are inserted before uppercase characters (with the exception
5720
     * of the first character of the string), and in place of spaces, dashes,
5721
     * and underscores. Alpha delimiters are not converted to lowercase.
5722
     *
5723
     * @param string      $str                           <p>The input string.</p>
5724
     * @param string      $delimiter                     <p>Sequence used to separate parts of the string.</p>
5725
     * @param string      $encoding                      [optional] <p>Set the charset for e.g. "mb_" function</p>
5726
     * @param bool        $clean_utf8                    [optional] <p>Remove non UTF-8 chars from the string.</p>
5727
     * @param string|null $lang                          [optional] <p>Set the language for special cases: az, el, lt,
5728
     *                                                   tr</p>
5729
     * @param bool        $try_to_keep_the_string_length [optional] <p>true === try to keep the string length: e.g. ẞ ->
5730
     *                                                   ß</p>
5731
     *
5732
     * @psalm-pure
5733
     *
5734
     * @return string
5735
     */
5736 49
    public static function str_delimit(
5737
        string $str,
5738
        string $delimiter,
5739
        string $encoding = 'UTF-8',
5740
        bool $clean_utf8 = false,
5741
        string $lang = null,
5742
        bool $try_to_keep_the_string_length = false
5743
    ): string {
5744 49
        if (self::$SUPPORT['mbstring'] === true) {
5745
            /** @noinspection PhpComposerExtensionStubsInspection */
5746 49
            $str = (string) \mb_ereg_replace('\\B(\\p{Lu})', '-\1', \trim($str));
5747
5748 49
            $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5749 49
            if ($use_mb_functions === true && $encoding === 'UTF-8') {
5750 22
                $str = \mb_strtolower($str);
5751
            } else {
5752 27
                $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5753
            }
5754
5755
            /** @noinspection PhpComposerExtensionStubsInspection */
5756 49
            return (string) \mb_ereg_replace('[\\-_\\s]+', $delimiter, $str);
5757
        }
5758
5759
        $str = (string) \preg_replace('/\\B(\\p{Lu})/u', '-\1', \trim($str));
5760
5761
        $use_mb_functions = $lang === null && $try_to_keep_the_string_length === false;
5762
        if ($use_mb_functions === true && $encoding === 'UTF-8') {
5763
            $str = \mb_strtolower($str);
5764
        } else {
5765
            $str = self::strtolower($str, $encoding, $clean_utf8, $lang, $try_to_keep_the_string_length);
5766
        }
5767
5768
        return (string) \preg_replace('/[\\-_\\s]+/u', $delimiter, $str);
5769
    }
5770
5771
    /**
5772
     * Optimized "mb_detect_encoding()"-function -> with support for UTF-16 and UTF-32.
5773
     *
5774
     * @param string $str <p>The input string.</p>
5775
     *
5776
     * @psalm-pure
5777
     *
5778
     * @return false|string
5779
     *                      The detected string-encoding e.g. UTF-8 or UTF-16BE,<br>
5780
     *                      otherwise it will return false e.g. for BINARY or not detected encoding.
5781
     */
5782 30
    public static function str_detect_encoding($str)
5783
    {
5784
        // init
5785 30
        $str = (string) $str;
5786
5787
        //
5788
        // 1.) check binary strings (010001001...) like UTF-16 / UTF-32 / PDF / Images / ...
5789
        //
5790
5791 30
        if (self::is_binary($str, true) === true) {
5792 11
            $is_utf32 = self::is_utf32($str, false);
5793 11
            if ($is_utf32 === 1) {
5794
                return 'UTF-32LE';
5795
            }
5796 11
            if ($is_utf32 === 2) {
5797 1
                return 'UTF-32BE';
5798
            }
5799
5800 11
            $is_utf16 = self::is_utf16($str, false);
5801 11
            if ($is_utf16 === 1) {
5802 3
                return 'UTF-16LE';
5803
            }
5804 11
            if ($is_utf16 === 2) {
5805 2
                return 'UTF-16BE';
5806
            }
5807
5808
            // is binary but not "UTF-16" or "UTF-32"
5809 9
            return false;
5810
        }
5811
5812
        //
5813
        // 2.) simple check for ASCII chars
5814
        //
5815
5816 26
        if (ASCII::is_ascii($str) === true) {
5817 10
            return 'ASCII';
5818
        }
5819
5820
        //
5821
        // 3.) simple check for UTF-8 chars
5822
        //
5823
5824 26
        if (self::is_utf8_string($str) === true) {
5825 19
            return 'UTF-8';
5826
        }
5827
5828
        //
5829
        // 4.) check via "mb_detect_encoding()"
5830
        //
5831
        // INFO: UTF-16, UTF-32, UCS2 and UCS4, encoding detection will fail always with "mb_detect_encoding()"
5832
5833
        $encoding_detecting_order = [
5834 15
            'ISO-8859-1',
5835
            'ISO-8859-2',
5836
            'ISO-8859-3',
5837
            'ISO-8859-4',
5838
            'ISO-8859-5',
5839
            'ISO-8859-6',
5840
            'ISO-8859-7',
5841
            'ISO-8859-8',
5842
            'ISO-8859-9',
5843
            'ISO-8859-10',
5844
            'ISO-8859-13',
5845
            'ISO-8859-14',
5846
            'ISO-8859-15',
5847
            'ISO-8859-16',
5848
            'WINDOWS-1251',
5849
            'WINDOWS-1252',
5850
            'WINDOWS-1254',
5851
            'CP932',
5852
            'CP936',
5853
            'CP950',
5854
            'CP866',
5855
            'CP850',
5856
            'CP51932',
5857
            'CP50220',
5858
            'CP50221',
5859
            'CP50222',
5860
            'ISO-2022-JP',
5861
            'ISO-2022-KR',
5862
            'JIS',
5863
            'JIS-ms',
5864
            'EUC-CN',
5865
            'EUC-JP',
5866
        ];
5867
5868 15
        if (self::$SUPPORT['mbstring'] === true) {
5869
            // info: do not use the symfony polyfill here
5870 15
            $encoding = \mb_detect_encoding($str, $encoding_detecting_order, true);
5871 15
            if ($encoding) {
5872 15
                return $encoding;
5873
            }
5874
        }
5875
5876
        //
5877
        // 5.) check via "iconv()"
5878
        //
5879
5880
        if (self::$ENCODINGS === null) {
5881
            self::$ENCODINGS = self::getData('encodings');
5882
        }
5883
5884
        foreach (self::$ENCODINGS as $encoding_tmp) {
5885
            // INFO: //IGNORE but still throw notice
5886
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
5887
            if ((string) @\iconv($encoding_tmp, $encoding_tmp . '//IGNORE', $str) === $str) {
5888
                return $encoding_tmp;
5889
            }
5890
        }
5891
5892
        return false;
5893
    }
5894
5895
    /**
5896
     * alias for "UTF8::str_ends_with()"
5897
     *
5898
     * @param string $haystack
5899
     * @param string $needle
5900
     *
5901
     * @psalm-pure
5902
     *
5903
     * @return bool
5904
     *
5905
     * @see        UTF8::str_ends_with()
5906
     * @deprecated <p>please use "UTF8::str_ends_with()"</p>
5907
     */
5908
    public static function str_ends(string $haystack, string $needle): bool
5909
    {
5910
        return self::str_ends_with($haystack, $needle);
5911
    }
5912
5913
    /**
5914
     * Check if the string ends with the given substring.
5915
     *
5916
     * @param string $haystack <p>The string to search in.</p>
5917
     * @param string $needle   <p>The substring to search for.</p>
5918
     *
5919
     * @psalm-pure
5920
     *
5921
     * @return bool
5922
     */
5923 9
    public static function str_ends_with(string $haystack, string $needle): bool
5924
    {
5925 9
        if ($needle === '') {
5926 2
            return true;
5927
        }
5928
5929 9
        if ($haystack === '') {
5930
            return false;
5931
        }
5932
5933 9
        return \substr($haystack, -\strlen($needle)) === $needle;
5934
    }
5935
5936
    /**
5937
     * Returns true if the string ends with any of $substrings, false otherwise.
5938
     *
5939
     * - case-sensitive
5940
     *
5941
     * @param string   $str        <p>The input string.</p>
5942
     * @param string[] $substrings <p>Substrings to look for.</p>
5943
     *
5944
     * @psalm-pure
5945
     *
5946
     * @return bool whether or not $str ends with $substring
5947
     */
5948 7
    public static function str_ends_with_any(string $str, array $substrings): bool
5949
    {
5950 7
        if ($substrings === []) {
5951
            return false;
5952
        }
5953
5954 7
        foreach ($substrings as &$substring) {
5955 7
            if (\substr($str, -\strlen($substring)) === $substring) {
5956 7
                return true;
5957
            }
5958
        }
5959
5960 6
        return false;
5961
    }
5962
5963
    /**
5964
     * Ensures that the string begins with $substring. If it doesn't, it's
5965
     * prepended.
5966
     *
5967
     * @param string $str       <p>The input string.</p>
5968
     * @param string $substring <p>The substring to add if not present.</p>
5969
     *
5970
     * @psalm-pure
5971
     *
5972
     * @return string
5973
     */
5974 10
    public static function str_ensure_left(string $str, string $substring): string
5975
    {
5976
        if (
5977 10
            $substring !== ''
5978
            &&
5979 10
            \strpos($str, $substring) === 0
5980
        ) {
5981 6
            return $str;
5982
        }
5983
5984 4
        return $substring . $str;
5985
    }
5986
5987
    /**
5988
     * Ensures that the string ends with $substring. If it doesn't, it's appended.
5989
     *
5990
     * @param string $str       <p>The input string.</p>
5991
     * @param string $substring <p>The substring to add if not present.</p>
5992
     *
5993
     * @psalm-pure
5994
     *
5995
     * @return string
5996
     */
5997 10
    public static function str_ensure_right(string $str, string $substring): string
5998
    {
5999
        if (
6000 10
            $str === ''
6001
            ||
6002 10
            $substring === ''
6003
            ||
6004 10
            \substr($str, -\strlen($substring)) !== $substring
6005
        ) {
6006 4
            $str .= $substring;
6007
        }
6008
6009 10
        return $str;
6010
    }
6011
6012
    /**
6013
     * Capitalizes the first word of the string, replaces underscores with
6014
     * spaces, and strips '_id'.
6015
     *
6016
     * @param string $str
6017
     *
6018
     * @psalm-pure
6019
     *
6020
     * @return string
6021
     */
6022 3
    public static function str_humanize($str): string
6023
    {
6024 3
        $str = \str_replace(
6025
            [
6026 3
                '_id',
6027
                '_',
6028
            ],
6029
            [
6030 3
                '',
6031
                ' ',
6032
            ],
6033 3
            $str
6034
        );
6035
6036 3
        return self::ucfirst(\trim($str));
6037
    }
6038
6039
    /**
6040
     * alias for "UTF8::str_istarts_with()"
6041
     *
6042
     * @param string $haystack
6043
     * @param string $needle
6044
     *
6045
     * @psalm-pure
6046
     *
6047
     * @return bool
6048
     *
6049
     * @see        UTF8::str_istarts_with()
6050
     * @deprecated <p>please use "UTF8::str_istarts_with()"</p>
6051
     */
6052
    public static function str_ibegins(string $haystack, string $needle): bool
6053
    {
6054
        return self::str_istarts_with($haystack, $needle);
6055
    }
6056
6057
    /**
6058
     * alias for "UTF8::str_iends_with()"
6059
     *
6060
     * @param string $haystack
6061
     * @param string $needle
6062
     *
6063
     * @psalm-pure
6064
     *
6065
     * @return bool
6066
     *
6067
     * @see        UTF8::str_iends_with()
6068
     * @deprecated <p>please use "UTF8::str_iends_with()"</p>
6069
     */
6070
    public static function str_iends(string $haystack, string $needle): bool
6071
    {
6072
        return self::str_iends_with($haystack, $needle);
6073
    }
6074
6075
    /**
6076
     * Check if the string ends with the given substring, case-insensitive.
6077
     *
6078
     * @param string $haystack <p>The string to search in.</p>
6079
     * @param string $needle   <p>The substring to search for.</p>
6080
     *
6081
     * @psalm-pure
6082
     *
6083
     * @return bool
6084
     */
6085 12
    public static function str_iends_with(string $haystack, string $needle): bool
6086
    {
6087 12
        if ($needle === '') {
6088 2
            return true;
6089
        }
6090
6091 12
        if ($haystack === '') {
6092
            return false;
6093
        }
6094
6095 12
        return self::strcasecmp(\substr($haystack, -\strlen($needle)), $needle) === 0;
6096
    }
6097
6098
    /**
6099
     * Returns true if the string ends with any of $substrings, false otherwise.
6100
     *
6101
     * - case-insensitive
6102
     *
6103
     * @param string   $str        <p>The input string.</p>
6104
     * @param string[] $substrings <p>Substrings to look for.</p>
6105
     *
6106
     * @psalm-pure
6107
     *
6108
     * @return bool
6109
     *              <p>Whether or not $str ends with $substring.</p>
6110
     */
6111 4
    public static function str_iends_with_any(string $str, array $substrings): bool
6112
    {
6113 4
        if ($substrings === []) {
6114
            return false;
6115
        }
6116
6117 4
        foreach ($substrings as &$substring) {
6118 4
            if (self::str_iends_with($str, $substring)) {
6119 4
                return true;
6120
            }
6121
        }
6122
6123
        return false;
6124
    }
6125
6126
    /**
6127
     * Returns the index of the first occurrence of $needle in the string,
6128
     * and false if not found. Accepts an optional offset from which to begin
6129
     * the search.
6130
     *
6131
     * @param string $str      <p>The input string.</p>
6132
     * @param string $needle   <p>Substring to look for.</p>
6133
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6134
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6135
     *
6136
     * @psalm-pure
6137
     *
6138
     * @return false|int
6139
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6140
     *
6141
     * @see        UTF8::stripos()
6142
     * @deprecated <p>please use "UTF8::stripos()"</p>
6143
     */
6144
    public static function str_iindex_first(
6145
        string $str,
6146
        string $needle,
6147
        int $offset = 0,
6148
        string $encoding = 'UTF-8'
6149
    ) {
6150
        return self::stripos(
6151
            $str,
6152
            $needle,
6153
            $offset,
6154
            $encoding
6155
        );
6156
    }
6157
6158
    /**
6159
     * Returns the index of the last occurrence of $needle in the string,
6160
     * and false if not found. Accepts an optional offset from which to begin
6161
     * the search. Offsets may be negative to count from the last character
6162
     * in the string.
6163
     *
6164
     * @param string $str      <p>The input string.</p>
6165
     * @param string $needle   <p>Substring to look for.</p>
6166
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6167
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6168
     *
6169
     * @psalm-pure
6170
     *
6171
     * @return false|int
6172
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6173
     *
6174
     * @see        UTF8::strripos()
6175
     * @deprecated <p>please use "UTF8::strripos()"</p>
6176
     */
6177
    public static function str_iindex_last(
6178
        string $str,
6179
        string $needle,
6180
        int $offset = 0,
6181
        string $encoding = 'UTF-8'
6182
    ) {
6183
        return self::strripos(
6184
            $str,
6185
            $needle,
6186
            $offset,
6187
            $encoding
6188
        );
6189
    }
6190
6191
    /**
6192
     * Returns the index of the first occurrence of $needle in the string,
6193
     * and false if not found. Accepts an optional offset from which to begin
6194
     * the search.
6195
     *
6196
     * @param string $str      <p>The input string.</p>
6197
     * @param string $needle   <p>Substring to look for.</p>
6198
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6199
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6200
     *
6201
     * @psalm-pure
6202
     *
6203
     * @return false|int
6204
     *                   <p>The occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6205
     *
6206
     * @see        UTF8::strpos()
6207
     * @deprecated <p>please use "UTF8::strpos()"</p>
6208
     */
6209 10
    public static function str_index_first(
6210
        string $str,
6211
        string $needle,
6212
        int $offset = 0,
6213
        string $encoding = 'UTF-8'
6214
    ) {
6215 10
        return self::strpos(
6216 10
            $str,
6217 10
            $needle,
6218 10
            $offset,
6219 10
            $encoding
6220
        );
6221
    }
6222
6223
    /**
6224
     * Returns the index of the last occurrence of $needle in the string,
6225
     * and false if not found. Accepts an optional offset from which to begin
6226
     * the search. Offsets may be negative to count from the last character
6227
     * in the string.
6228
     *
6229
     * @param string $str      <p>The input string.</p>
6230
     * @param string $needle   <p>Substring to look for.</p>
6231
     * @param int    $offset   [optional] <p>Offset from which to search. Default: 0</p>
6232
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6233
     *
6234
     * @psalm-pure
6235
     *
6236
     * @return false|int
6237
     *                   <p>The last occurrence's <strong>index</strong> if found, otherwise <strong>false</strong>.</p>
6238
     *
6239
     * @see        UTF8::strrpos()
6240
     * @deprecated <p>please use "UTF8::strrpos()"</p>
6241
     */
6242 10
    public static function str_index_last(
6243
        string $str,
6244
        string $needle,
6245
        int $offset = 0,
6246
        string $encoding = 'UTF-8'
6247
    ) {
6248 10
        return self::strrpos(
6249 10
            $str,
6250 10
            $needle,
6251 10
            $offset,
6252 10
            $encoding
6253
        );
6254
    }
6255
6256
    /**
6257
     * Inserts $substring into the string at the $index provided.
6258
     *
6259
     * @param string $str       <p>The input string.</p>
6260
     * @param string $substring <p>String to be inserted.</p>
6261
     * @param int    $index     <p>The index at which to insert the substring.</p>
6262
     * @param string $encoding  [optional] <p>Set the charset for e.g. "mb_" function</p>
6263
     *
6264
     * @psalm-pure
6265
     *
6266
     * @return string
6267
     */
6268 8
    public static function str_insert(
6269
        string $str,
6270
        string $substring,
6271
        int $index,
6272
        string $encoding = 'UTF-8'
6273
    ): string {
6274 8
        if ($encoding === 'UTF-8') {
6275 4
            $len = (int) \mb_strlen($str);
6276 4
            if ($index > $len) {
6277
                return $str;
6278
            }
6279
6280
            /** @noinspection UnnecessaryCastingInspection */
6281 4
            return (string) \mb_substr($str, 0, $index) .
6282 4
                   $substring .
6283 4
                   (string) \mb_substr($str, $index, $len);
6284
        }
6285
6286 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6287
6288 4
        $len = (int) self::strlen($str, $encoding);
6289 4
        if ($index > $len) {
6290 1
            return $str;
6291
        }
6292
6293 3
        return ((string) self::substr($str, 0, $index, $encoding)) .
6294 3
               $substring .
6295 3
               ((string) self::substr($str, $index, $len, $encoding));
6296
    }
6297
6298
    /**
6299
     * Case-insensitive and UTF-8 safe version of <function>str_replace</function>.
6300
     *
6301
     * @see http://php.net/manual/en/function.str-ireplace.php
6302
     *
6303
     * @param mixed $search  <p>
6304
     *                       Every replacement with search array is
6305
     *                       performed on the result of previous replacement.
6306
     *                       </p>
6307
     * @param mixed $replace <p>
6308
     *                       </p>
6309
     * @param mixed $subject <p>
6310
     *                       If subject is an array, then the search and
6311
     *                       replace is performed with every entry of
6312
     *                       subject, and the return value is an array as
6313
     *                       well.
6314
     *                       </p>
6315
     * @param int   $count   [optional] <p>
6316
     *                       The number of matched and replaced needles will
6317
     *                       be returned in count which is passed by
6318
     *                       reference.
6319
     *                       </p>
6320
     *
6321
     * @psalm-pure
6322
     *
6323
     * @return mixed a string or an array of replacements
6324
     */
6325 29
    public static function str_ireplace($search, $replace, $subject, &$count = null)
6326
    {
6327 29
        $search = (array) $search;
6328
6329
        /** @noinspection AlterInForeachInspection */
6330 29
        foreach ($search as &$s) {
6331 29
            $s = (string) $s;
6332 29
            if ($s === '') {
6333 6
                $s = '/^(?<=.)$/';
6334
            } else {
6335 29
                $s = '/' . \preg_quote($s, '/') . '/ui';
6336
            }
6337
        }
6338
6339 29
        $subject = \preg_replace($search, $replace, $subject, -1, $replace);
6340 29
        $count = $replace; // used as reference parameter
6341
6342 29
        return $subject;
6343
    }
6344
6345
    /**
6346
     * Replaces $search from the beginning of string with $replacement.
6347
     *
6348
     * @param string $str         <p>The input string.</p>
6349
     * @param string $search      <p>The string to search for.</p>
6350
     * @param string $replacement <p>The replacement.</p>
6351
     *
6352
     * @psalm-pure
6353
     *
6354
     * @return string string after the replacements
6355
     */
6356 17
    public static function str_ireplace_beginning(string $str, string $search, string $replacement): string
6357
    {
6358 17
        if ($str === '') {
6359 4
            if ($replacement === '') {
6360 2
                return '';
6361
            }
6362
6363 2
            if ($search === '') {
6364 2
                return $replacement;
6365
            }
6366
        }
6367
6368 13
        if ($search === '') {
6369 2
            return $str . $replacement;
6370
        }
6371
6372 11
        if (\stripos($str, $search) === 0) {
6373 10
            return $replacement . \substr($str, \strlen($search));
6374
        }
6375
6376 1
        return $str;
6377
    }
6378
6379
    /**
6380
     * Replaces $search from the ending of string with $replacement.
6381
     *
6382
     * @param string $str         <p>The input string.</p>
6383
     * @param string $search      <p>The string to search for.</p>
6384
     * @param string $replacement <p>The replacement.</p>
6385
     *
6386
     * @psalm-pure
6387
     *
6388
     * @return string
6389
     *                <p>string after the replacements.</p>
6390
     */
6391 17
    public static function str_ireplace_ending(string $str, string $search, string $replacement): string
6392
    {
6393 17
        if ($str === '') {
6394 4
            if ($replacement === '') {
6395 2
                return '';
6396
            }
6397
6398 2
            if ($search === '') {
6399 2
                return $replacement;
6400
            }
6401
        }
6402
6403 13
        if ($search === '') {
6404 2
            return $str . $replacement;
6405
        }
6406
6407 11
        if (\stripos($str, $search, \strlen($str) - \strlen($search)) !== false) {
6408 9
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
6409
        }
6410
6411 11
        return $str;
6412
    }
6413
6414
    /**
6415
     * Check if the string starts with the given substring, case-insensitive.
6416
     *
6417
     * @param string $haystack <p>The string to search in.</p>
6418
     * @param string $needle   <p>The substring to search for.</p>
6419
     *
6420
     * @psalm-pure
6421
     *
6422
     * @return bool
6423
     */
6424 13
    public static function str_istarts_with(string $haystack, string $needle): bool
6425
    {
6426 13
        if ($needle === '') {
6427 2
            return true;
6428
        }
6429
6430 13
        if ($haystack === '') {
6431
            return false;
6432
        }
6433
6434 13
        return self::stripos($haystack, $needle) === 0;
6435
    }
6436
6437
    /**
6438
     * Returns true if the string begins with any of $substrings, false otherwise.
6439
     *
6440
     * - case-insensitive
6441
     *
6442
     * @param string $str        <p>The input string.</p>
6443
     * @param array  $substrings <p>Substrings to look for.</p>
6444
     *
6445
     * @psalm-pure
6446
     *
6447
     * @return bool whether or not $str starts with $substring
6448
     */
6449 5
    public static function str_istarts_with_any(string $str, array $substrings): bool
6450
    {
6451 5
        if ($str === '') {
6452
            return false;
6453
        }
6454
6455 5
        if ($substrings === []) {
6456
            return false;
6457
        }
6458
6459 5
        foreach ($substrings as &$substring) {
6460 5
            if (self::str_istarts_with($str, $substring)) {
6461 5
                return true;
6462
            }
6463
        }
6464
6465 1
        return false;
6466
    }
6467
6468
    /**
6469
     * Gets the substring after the first occurrence of a separator.
6470
     *
6471
     * @param string $str       <p>The input string.</p>
6472
     * @param string $separator <p>The string separator.</p>
6473
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6474
     *
6475
     * @psalm-pure
6476
     *
6477
     * @return string
6478
     */
6479 1
    public static function str_isubstr_after_first_separator(
6480
        string $str,
6481
        string $separator,
6482
        string $encoding = 'UTF-8'
6483
    ): string {
6484 1
        if ($separator === '' || $str === '') {
6485 1
            return '';
6486
        }
6487
6488 1
        $offset = self::stripos($str, $separator);
6489 1
        if ($offset === false) {
6490 1
            return '';
6491
        }
6492
6493 1
        if ($encoding === 'UTF-8') {
6494 1
            return (string) \mb_substr(
6495 1
                $str,
6496 1
                $offset + (int) \mb_strlen($separator)
6497
            );
6498
        }
6499
6500
        return (string) self::substr(
6501
            $str,
6502
            $offset + (int) self::strlen($separator, $encoding),
6503
            null,
6504
            $encoding
6505
        );
6506
    }
6507
6508
    /**
6509
     * Gets the substring after the last occurrence of a separator.
6510
     *
6511
     * @param string $str       <p>The input string.</p>
6512
     * @param string $separator <p>The string separator.</p>
6513
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6514
     *
6515
     * @psalm-pure
6516
     *
6517
     * @return string
6518
     */
6519 1
    public static function str_isubstr_after_last_separator(
6520
        string $str,
6521
        string $separator,
6522
        string $encoding = 'UTF-8'
6523
    ): string {
6524 1
        if ($separator === '' || $str === '') {
6525 1
            return '';
6526
        }
6527
6528 1
        $offset = self::strripos($str, $separator);
6529 1
        if ($offset === false) {
6530 1
            return '';
6531
        }
6532
6533 1
        if ($encoding === 'UTF-8') {
6534 1
            return (string) \mb_substr(
6535 1
                $str,
6536 1
                $offset + (int) self::strlen($separator)
6537
            );
6538
        }
6539
6540
        return (string) self::substr(
6541
            $str,
6542
            $offset + (int) self::strlen($separator, $encoding),
6543
            null,
6544
            $encoding
6545
        );
6546
    }
6547
6548
    /**
6549
     * Gets the substring before the first occurrence of a separator.
6550
     *
6551
     * @param string $str       <p>The input string.</p>
6552
     * @param string $separator <p>The string separator.</p>
6553
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6554
     *
6555
     * @psalm-pure
6556
     *
6557
     * @return string
6558
     */
6559 1
    public static function str_isubstr_before_first_separator(
6560
        string $str,
6561
        string $separator,
6562
        string $encoding = 'UTF-8'
6563
    ): string {
6564 1
        if ($separator === '' || $str === '') {
6565 1
            return '';
6566
        }
6567
6568 1
        $offset = self::stripos($str, $separator);
6569 1
        if ($offset === false) {
6570 1
            return '';
6571
        }
6572
6573 1
        if ($encoding === 'UTF-8') {
6574 1
            return (string) \mb_substr($str, 0, $offset);
6575
        }
6576
6577
        return (string) self::substr($str, 0, $offset, $encoding);
6578
    }
6579
6580
    /**
6581
     * Gets the substring before the last occurrence of a separator.
6582
     *
6583
     * @param string $str       <p>The input string.</p>
6584
     * @param string $separator <p>The string separator.</p>
6585
     * @param string $encoding  [optional] <p>Default: 'UTF-8'</p>
6586
     *
6587
     * @psalm-pure
6588
     *
6589
     * @return string
6590
     */
6591 1
    public static function str_isubstr_before_last_separator(
6592
        string $str,
6593
        string $separator,
6594
        string $encoding = 'UTF-8'
6595
    ): string {
6596 1
        if ($separator === '' || $str === '') {
6597 1
            return '';
6598
        }
6599
6600 1
        if ($encoding === 'UTF-8') {
6601 1
            $offset = \mb_strripos($str, $separator);
6602 1
            if ($offset === false) {
6603 1
                return '';
6604
            }
6605
6606 1
            return (string) \mb_substr($str, 0, $offset);
6607
        }
6608
6609
        $offset = self::strripos($str, $separator, 0, $encoding);
6610
        if ($offset === false) {
6611
            return '';
6612
        }
6613
6614
        return (string) self::substr($str, 0, $offset, $encoding);
6615
    }
6616
6617
    /**
6618
     * Gets the substring after (or before via "$before_needle") the first occurrence of the "$needle".
6619
     *
6620
     * @param string $str           <p>The input string.</p>
6621
     * @param string $needle        <p>The string to look for.</p>
6622
     * @param bool   $before_needle [optional] <p>Default: false</p>
6623
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6624
     *
6625
     * @psalm-pure
6626
     *
6627
     * @return string
6628
     */
6629 2
    public static function str_isubstr_first(
6630
        string $str,
6631
        string $needle,
6632
        bool $before_needle = false,
6633
        string $encoding = 'UTF-8'
6634
    ): string {
6635
        if (
6636 2
            $needle === ''
6637
            ||
6638 2
            $str === ''
6639
        ) {
6640 2
            return '';
6641
        }
6642
6643 2
        $part = self::stristr(
6644 2
            $str,
6645 2
            $needle,
6646 2
            $before_needle,
6647 2
            $encoding
6648
        );
6649 2
        if ($part === false) {
6650 2
            return '';
6651
        }
6652
6653 2
        return $part;
6654
    }
6655
6656
    /**
6657
     * Gets the substring after (or before via "$before_needle") the last occurrence of the "$needle".
6658
     *
6659
     * @param string $str           <p>The input string.</p>
6660
     * @param string $needle        <p>The string to look for.</p>
6661
     * @param bool   $before_needle [optional] <p>Default: false</p>
6662
     * @param string $encoding      [optional] <p>Default: 'UTF-8'</p>
6663
     *
6664
     * @psalm-pure
6665
     *
6666
     * @return string
6667
     */
6668 1
    public static function str_isubstr_last(
6669
        string $str,
6670
        string $needle,
6671
        bool $before_needle = false,
6672
        string $encoding = 'UTF-8'
6673
    ): string {
6674
        if (
6675 1
            $needle === ''
6676
            ||
6677 1
            $str === ''
6678
        ) {
6679 1
            return '';
6680
        }
6681
6682 1
        $part = self::strrichr(
6683 1
            $str,
6684 1
            $needle,
6685 1
            $before_needle,
6686 1
            $encoding
6687
        );
6688 1
        if ($part === false) {
6689 1
            return '';
6690
        }
6691
6692 1
        return $part;
6693
    }
6694
6695
    /**
6696
     * Returns the last $n characters of the string.
6697
     *
6698
     * @param string $str      <p>The input string.</p>
6699
     * @param int    $n        <p>Number of characters to retrieve from the end.</p>
6700
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6701
     *
6702
     * @psalm-pure
6703
     *
6704
     * @return string
6705
     */
6706 12
    public static function str_last_char(
6707
        string $str,
6708
        int $n = 1,
6709
        string $encoding = 'UTF-8'
6710
    ): string {
6711 12
        if ($str === '' || $n <= 0) {
6712 4
            return '';
6713
        }
6714
6715 8
        if ($encoding === 'UTF-8') {
6716 4
            return (string) \mb_substr($str, -$n);
6717
        }
6718
6719 4
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6720
6721 4
        return (string) self::substr($str, -$n, null, $encoding);
6722
    }
6723
6724
    /**
6725
     * Limit the number of characters in a string.
6726
     *
6727
     * @param string $str        <p>The input string.</p>
6728
     * @param int    $length     [optional] <p>Default: 100</p>
6729
     * @param string $str_add_on [optional] <p>Default: …</p>
6730
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6731
     *
6732
     * @psalm-pure
6733
     *
6734
     * @return string
6735
     */
6736 2
    public static function str_limit(
6737
        string $str,
6738
        int $length = 100,
6739
        string $str_add_on = '…',
6740
        string $encoding = 'UTF-8'
6741
    ): string {
6742 2
        if ($str === '' || $length <= 0) {
6743 2
            return '';
6744
        }
6745
6746 2
        if ($encoding === 'UTF-8') {
6747 2
            if ((int) \mb_strlen($str) <= $length) {
6748 2
                return $str;
6749
            }
6750
6751
            /** @noinspection UnnecessaryCastingInspection */
6752 2
            return (string) \mb_substr($str, 0, $length - (int) self::strlen($str_add_on)) . $str_add_on;
6753
        }
6754
6755
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
6756
6757
        if ((int) self::strlen($str, $encoding) <= $length) {
6758
            return $str;
6759
        }
6760
6761
        return ((string) self::substr($str, 0, $length - (int) self::strlen($str_add_on), $encoding)) . $str_add_on;
6762
    }
6763
6764
    /**
6765
     * Limit the number of characters in a string, but also after the next word.
6766
     *
6767
     * @param string $str        <p>The input string.</p>
6768
     * @param int    $length     [optional] <p>Default: 100</p>
6769
     * @param string $str_add_on [optional] <p>Default: …</p>
6770
     * @param string $encoding   [optional] <p>Set the charset for e.g. "mb_" function</p>
6771
     *
6772
     * @psalm-pure
6773
     *
6774
     * @return string
6775
     */
6776 6
    public static function str_limit_after_word(
6777
        string $str,
6778
        int $length = 100,
6779
        string $str_add_on = '…',
6780
        string $encoding = 'UTF-8'
6781
    ): string {
6782 6
        if ($str === '' || $length <= 0) {
6783 2
            return '';
6784
        }
6785
6786 6
        if ($encoding === 'UTF-8') {
6787
            /** @noinspection UnnecessaryCastingInspection */
6788 2
            if ((int) \mb_strlen($str) <= $length) {
6789 2
                return $str;
6790
            }
6791
6792 2
            if (\mb_substr($str, $length - 1, 1) === ' ') {
6793 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6794
            }
6795
6796 2
            $str = \mb_substr($str, 0, $length);
6797
6798 2
            $array = \explode(' ', $str);
6799 2
            \array_pop($array);
6800 2
            $new_str = \implode(' ', $array);
6801
6802 2
            if ($new_str === '') {
6803 2
                return ((string) \mb_substr($str, 0, $length - 1)) . $str_add_on;
6804
            }
6805
        } else {
6806 4
            if ((int) self::strlen($str, $encoding) <= $length) {
6807
                return $str;
6808
            }
6809
6810 4
            if (self::substr($str, $length - 1, 1, $encoding) === ' ') {
6811 3
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6812
            }
6813
6814
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6815 1
            $str = self::substr($str, 0, $length, $encoding);
6816
            /** @noinspection CallableParameterUseCaseInTypeContextInspection - FP */
6817 1
            if ($str === false) {
6818
                return '' . $str_add_on;
6819
            }
6820
6821 1
            $array = \explode(' ', $str);
6822 1
            \array_pop($array);
6823 1
            $new_str = \implode(' ', $array);
6824
6825 1
            if ($new_str === '') {
6826
                return ((string) self::substr($str, 0, $length - 1, $encoding)) . $str_add_on;
6827
            }
6828
        }
6829
6830 3
        return $new_str . $str_add_on;
6831
    }
6832
6833
    /**
6834
     * Returns the longest common prefix between the $str1 and $str2.
6835
     *
6836
     * @param string $str1     <p>The input sting.</p>
6837
     * @param string $str2     <p>Second string for comparison.</p>
6838
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6839
     *
6840
     * @psalm-pure
6841
     *
6842
     * @return string
6843
     */
6844 10
    public static function str_longest_common_prefix(
6845
        string $str1,
6846
        string $str2,
6847
        string $encoding = 'UTF-8'
6848
    ): string {
6849
        // init
6850 10
        $longest_common_prefix = '';
6851
6852 10
        if ($encoding === 'UTF-8') {
6853 5
            $max_length = (int) \min(
6854 5
                \mb_strlen($str1),
6855 5
                \mb_strlen($str2)
6856
            );
6857
6858 5
            for ($i = 0; $i < $max_length; ++$i) {
6859 4
                $char = \mb_substr($str1, $i, 1);
6860
6861
                if (
6862 4
                    $char !== false
6863
                    &&
6864 4
                    $char === \mb_substr($str2, $i, 1)
6865
                ) {
6866 3
                    $longest_common_prefix .= $char;
6867
                } else {
6868 3
                    break;
6869
                }
6870
            }
6871
        } else {
6872 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6873
6874 5
            $max_length = (int) \min(
6875 5
                self::strlen($str1, $encoding),
6876 5
                self::strlen($str2, $encoding)
6877
            );
6878
6879 5
            for ($i = 0; $i < $max_length; ++$i) {
6880 4
                $char = self::substr($str1, $i, 1, $encoding);
6881
6882
                if (
6883 4
                    $char !== false
6884
                    &&
6885 4
                    $char === self::substr($str2, $i, 1, $encoding)
6886
                ) {
6887 3
                    $longest_common_prefix .= $char;
6888
                } else {
6889 3
                    break;
6890
                }
6891
            }
6892
        }
6893
6894 10
        return $longest_common_prefix;
6895
    }
6896
6897
    /**
6898
     * Returns the longest common substring between the $str1 and $str2.
6899
     * In the case of ties, it returns that which occurs first.
6900
     *
6901
     * @param string $str1
6902
     * @param string $str2     <p>Second string for comparison.</p>
6903
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6904
     *
6905
     * @psalm-pure
6906
     *
6907
     * @return string
6908
     *                <p>A string with its $str being the longest common substring.</p>
6909
     */
6910 11
    public static function str_longest_common_substring(
6911
        string $str1,
6912
        string $str2,
6913
        string $encoding = 'UTF-8'
6914
    ): string {
6915 11
        if ($str1 === '' || $str2 === '') {
6916 2
            return '';
6917
        }
6918
6919
        // Uses dynamic programming to solve
6920
        // http://en.wikipedia.org/wiki/Longest_common_substring_problem
6921
6922 9
        if ($encoding === 'UTF-8') {
6923 4
            $str_length = (int) \mb_strlen($str1);
6924 4
            $other_length = (int) \mb_strlen($str2);
6925
        } else {
6926 5
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
6927
6928 5
            $str_length = (int) self::strlen($str1, $encoding);
6929 5
            $other_length = (int) self::strlen($str2, $encoding);
6930
        }
6931
6932
        // Return if either string is empty
6933 9
        if ($str_length === 0 || $other_length === 0) {
6934
            return '';
6935
        }
6936
6937 9
        $len = 0;
6938 9
        $end = 0;
6939 9
        $table = \array_fill(
6940 9
            0,
6941 9
            $str_length + 1,
6942 9
            \array_fill(0, $other_length + 1, 0)
6943
        );
6944
6945 9
        if ($encoding === 'UTF-8') {
6946 9
            for ($i = 1; $i <= $str_length; ++$i) {
6947 9
                for ($j = 1; $j <= $other_length; ++$j) {
6948 9
                    $str_char = \mb_substr($str1, $i - 1, 1);
6949 9
                    $other_char = \mb_substr($str2, $j - 1, 1);
6950
6951 9
                    if ($str_char === $other_char) {
6952 8
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6953 8
                        if ($table[$i][$j] > $len) {
6954 8
                            $len = $table[$i][$j];
6955 8
                            $end = $i;
6956
                        }
6957
                    } else {
6958 9
                        $table[$i][$j] = 0;
6959
                    }
6960
                }
6961
            }
6962
        } else {
6963
            for ($i = 1; $i <= $str_length; ++$i) {
6964
                for ($j = 1; $j <= $other_length; ++$j) {
6965
                    $str_char = self::substr($str1, $i - 1, 1, $encoding);
6966
                    $other_char = self::substr($str2, $j - 1, 1, $encoding);
6967
6968
                    if ($str_char === $other_char) {
6969
                        $table[$i][$j] = $table[$i - 1][$j - 1] + 1;
6970
                        if ($table[$i][$j] > $len) {
6971
                            $len = $table[$i][$j];
6972
                            $end = $i;
6973
                        }
6974
                    } else {
6975
                        $table[$i][$j] = 0;
6976
                    }
6977
                }
6978
            }
6979
        }
6980
6981 9
        if ($encoding === 'UTF-8') {
6982 9
            return (string) \mb_substr($str1, $end - $len, $len);
6983
        }
6984
6985
        return (string) self::substr($str1, $end - $len, $len, $encoding);
6986
    }
6987
6988
    /**
6989
     * Returns the longest common suffix between the $str1 and $str2.
6990
     *
6991
     * @param string $str1
6992
     * @param string $str2     <p>Second string for comparison.</p>
6993
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
6994
     *
6995
     * @psalm-pure
6996
     *
6997
     * @return string
6998
     */
6999 10
    public static function str_longest_common_suffix(
7000
        string $str1,
7001
        string $str2,
7002
        string $encoding = 'UTF-8'
7003
    ): string {
7004 10
        if ($str1 === '' || $str2 === '') {
7005 2
            return '';
7006
        }
7007
7008 8
        if ($encoding === 'UTF-8') {
7009 4
            $max_length = (int) \min(
7010 4
                \mb_strlen($str1, $encoding),
7011 4
                \mb_strlen($str2, $encoding)
7012
            );
7013
7014 4
            $longest_common_suffix = '';
7015 4
            for ($i = 1; $i <= $max_length; ++$i) {
7016 4
                $char = \mb_substr($str1, -$i, 1);
7017
7018
                if (
7019 4
                    $char !== false
7020
                    &&
7021 4
                    $char === \mb_substr($str2, -$i, 1)
7022
                ) {
7023 3
                    $longest_common_suffix = $char . $longest_common_suffix;
7024
                } else {
7025 3
                    break;
7026
                }
7027
            }
7028
        } else {
7029 4
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7030
7031 4
            $max_length = (int) \min(
7032 4
                self::strlen($str1, $encoding),
7033 4
                self::strlen($str2, $encoding)
7034
            );
7035
7036 4
            $longest_common_suffix = '';
7037 4
            for ($i = 1; $i <= $max_length; ++$i) {
7038 4
                $char = self::substr($str1, -$i, 1, $encoding);
7039
7040
                if (
7041 4
                    $char !== false
7042
                    &&
7043 4
                    $char === self::substr($str2, -$i, 1, $encoding)
7044
                ) {
7045 3
                    $longest_common_suffix = $char . $longest_common_suffix;
7046
                } else {
7047 3
                    break;
7048
                }
7049
            }
7050
        }
7051
7052 8
        return $longest_common_suffix;
7053
    }
7054
7055
    /**
7056
     * Returns true if $str matches the supplied pattern, false otherwise.
7057
     *
7058
     * @param string $str     <p>The input string.</p>
7059
     * @param string $pattern <p>Regex pattern to match against.</p>
7060
     *
7061
     * @psalm-pure
7062
     *
7063
     * @return bool whether or not $str matches the pattern
7064
     */
7065 10
    public static function str_matches_pattern(string $str, string $pattern): bool
7066
    {
7067 10
        return (bool) \preg_match('/' . $pattern . '/u', $str);
7068
    }
7069
7070
    /**
7071
     * Returns whether or not a character exists at an index. Offsets may be
7072
     * negative to count from the last character in the string. Implements
7073
     * part of the ArrayAccess interface.
7074
     *
7075
     * @param string $str      <p>The input string.</p>
7076
     * @param int    $offset   <p>The index to check.</p>
7077
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7078
     *
7079
     * @psalm-pure
7080
     *
7081
     * @return bool whether or not the index exists
7082
     */
7083 6
    public static function str_offset_exists(string $str, int $offset, string $encoding = 'UTF-8'): bool
7084
    {
7085
        // init
7086 6
        $length = (int) self::strlen($str, $encoding);
7087
7088 6
        if ($offset >= 0) {
7089 3
            return $length > $offset;
7090
        }
7091
7092 3
        return $length >= \abs($offset);
7093
    }
7094
7095
    /**
7096
     * Returns the character at the given index. Offsets may be negative to
7097
     * count from the last character in the string. Implements part of the
7098
     * ArrayAccess interface, and throws an OutOfBoundsException if the index
7099
     * does not exist.
7100
     *
7101
     * @param string $str      <p>The input string.</p>
7102
     * @param int    $index    <p>The <strong>index</strong> from which to retrieve the char.</p>
7103
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7104
     *
7105
     * @throws \OutOfBoundsException if the positive or negative offset does not exist
7106
     *
7107
     * @return string
7108
     *                <p>The character at the specified index.</p>
7109
     *
7110
     * @psalm-pure
7111
     */
7112 2
    public static function str_offset_get(string $str, int $index, string $encoding = 'UTF-8'): string
7113
    {
7114
        // init
7115 2
        $length = (int) self::strlen($str);
7116
7117
        if (
7118 2
            ($index >= 0 && $length <= $index)
7119
            ||
7120 2
            $length < \abs($index)
7121
        ) {
7122 1
            throw new \OutOfBoundsException('No character exists at the index');
7123
        }
7124
7125 1
        return self::char_at($str, $index, $encoding);
7126
    }
7127
7128
    /**
7129
     * Pad a UTF-8 string to a given length with another string.
7130
     *
7131
     * @param string     $str        <p>The input string.</p>
7132
     * @param int        $pad_length <p>The length of return string.</p>
7133
     * @param string     $pad_string [optional] <p>String to use for padding the input string.</p>
7134
     * @param int|string $pad_type   [optional] <p>
7135
     *                               Can be <strong>STR_PAD_RIGHT</strong> (default), [or string "right"]<br>
7136
     *                               <strong>STR_PAD_LEFT</strong> [or string "left"] or<br>
7137
     *                               <strong>STR_PAD_BOTH</strong> [or string "both"]
7138
     *                               </p>
7139
     * @param string     $encoding   [optional] <p>Default: 'UTF-8'</p>
7140
     *
7141
     * @psalm-pure
7142
     *
7143
     * @return string
7144
     *                <p>Returns the padded string.</p>
7145
     */
7146 41
    public static function str_pad(
7147
        string $str,
7148
        int $pad_length,
7149
        string $pad_string = ' ',
7150
        $pad_type = \STR_PAD_RIGHT,
7151
        string $encoding = 'UTF-8'
7152
    ): string {
7153 41
        if ($pad_length === 0 || $pad_string === '') {
7154 1
            return $str;
7155
        }
7156
7157 41
        if ($pad_type !== (int) $pad_type) {
7158 13
            if ($pad_type === 'left') {
7159 3
                $pad_type = \STR_PAD_LEFT;
7160 10
            } elseif ($pad_type === 'right') {
7161 6
                $pad_type = \STR_PAD_RIGHT;
7162 4
            } elseif ($pad_type === 'both') {
7163 3
                $pad_type = \STR_PAD_BOTH;
7164
            } else {
7165 1
                throw new \InvalidArgumentException(
7166 1
                    'Pad expects $pad_type to be "STR_PAD_*" or ' . "to be one of 'left', 'right' or 'both'"
7167
                );
7168
            }
7169
        }
7170
7171 40
        if ($encoding === 'UTF-8') {
7172 25
            $str_length = (int) \mb_strlen($str);
7173
7174 25
            if ($pad_length >= $str_length) {
7175
                switch ($pad_type) {
7176 25
                    case \STR_PAD_LEFT:
7177 8
                        $ps_length = (int) \mb_strlen($pad_string);
7178
7179 8
                        $diff = ($pad_length - $str_length);
7180
7181 8
                        $pre = (string) \mb_substr(
7182 8
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7183 8
                            0,
7184 8
                            $diff
7185
                        );
7186 8
                        $post = '';
7187
7188 8
                        break;
7189
7190 20
                    case \STR_PAD_BOTH:
7191 14
                        $diff = ($pad_length - $str_length);
7192
7193 14
                        $ps_length_left = (int) \floor($diff / 2);
7194
7195 14
                        $ps_length_right = (int) \ceil($diff / 2);
7196
7197 14
                        $pre = (string) \mb_substr(
7198 14
                            \str_repeat($pad_string, $ps_length_left),
7199 14
                            0,
7200 14
                            $ps_length_left
7201
                        );
7202 14
                        $post = (string) \mb_substr(
7203 14
                            \str_repeat($pad_string, $ps_length_right),
7204 14
                            0,
7205 14
                            $ps_length_right
7206
                        );
7207
7208 14
                        break;
7209
7210 9
                    case \STR_PAD_RIGHT:
7211
                    default:
7212 9
                        $ps_length = (int) \mb_strlen($pad_string);
7213
7214 9
                        $diff = ($pad_length - $str_length);
7215
7216 9
                        $post = (string) \mb_substr(
7217 9
                            \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7218 9
                            0,
7219 9
                            $diff
7220
                        );
7221 9
                        $pre = '';
7222
                }
7223
7224 25
                return $pre . $str . $post;
7225
            }
7226
7227 3
            return $str;
7228
        }
7229
7230 15
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7231
7232 15
        $str_length = (int) self::strlen($str, $encoding);
7233
7234 15
        if ($pad_length >= $str_length) {
7235
            switch ($pad_type) {
7236 14
                case \STR_PAD_LEFT:
7237 5
                    $ps_length = (int) self::strlen($pad_string, $encoding);
7238
7239 5
                    $diff = ($pad_length - $str_length);
7240
7241 5
                    $pre = (string) self::substr(
7242 5
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7243 5
                        0,
7244 5
                        $diff,
7245 5
                        $encoding
7246
                    );
7247 5
                    $post = '';
7248
7249 5
                    break;
7250
7251 9
                case \STR_PAD_BOTH:
7252 3
                    $diff = ($pad_length - $str_length);
7253
7254 3
                    $ps_length_left = (int) \floor($diff / 2);
7255
7256 3
                    $ps_length_right = (int) \ceil($diff / 2);
7257
7258 3
                    $pre = (string) self::substr(
7259 3
                        \str_repeat($pad_string, $ps_length_left),
7260 3
                        0,
7261 3
                        $ps_length_left,
7262 3
                        $encoding
7263
                    );
7264 3
                    $post = (string) self::substr(
7265 3
                        \str_repeat($pad_string, $ps_length_right),
7266 3
                        0,
7267 3
                        $ps_length_right,
7268 3
                        $encoding
7269
                    );
7270
7271 3
                    break;
7272
7273 6
                case \STR_PAD_RIGHT:
7274
                default:
7275 6
                    $ps_length = (int) self::strlen($pad_string, $encoding);
7276
7277 6
                    $diff = ($pad_length - $str_length);
7278
7279 6
                    $post = (string) self::substr(
7280 6
                        \str_repeat($pad_string, (int) \ceil($diff / $ps_length)),
7281 6
                        0,
7282 6
                        $diff,
7283 6
                        $encoding
7284
                    );
7285 6
                    $pre = '';
7286
            }
7287
7288 14
            return $pre . $str . $post;
7289
        }
7290
7291 1
        return $str;
7292
    }
7293
7294
    /**
7295
     * Returns a new string of a given length such that both sides of the
7296
     * string are padded. Alias for "UTF8::str_pad()" with a $pad_type of 'both'.
7297
     *
7298
     * @param string $str
7299
     * @param int    $length   <p>Desired string length after padding.</p>
7300
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7301
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7302
     *
7303
     * @psalm-pure
7304
     *
7305
     * @return string
7306
     *                <p>The string with padding applied.</p>
7307
     */
7308 11
    public static function str_pad_both(
7309
        string $str,
7310
        int $length,
7311
        string $pad_str = ' ',
7312
        string $encoding = 'UTF-8'
7313
    ): string {
7314 11
        return self::str_pad(
7315 11
            $str,
7316 11
            $length,
7317 11
            $pad_str,
7318 11
            \STR_PAD_BOTH,
7319 11
            $encoding
7320
        );
7321
    }
7322
7323
    /**
7324
     * Returns a new string of a given length such that the beginning of the
7325
     * string is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'left'.
7326
     *
7327
     * @param string $str
7328
     * @param int    $length   <p>Desired string length after padding.</p>
7329
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7330
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7331
     *
7332
     * @psalm-pure
7333
     *
7334
     * @return string
7335
     *                <p>The string with left padding.</p>
7336
     */
7337 7
    public static function str_pad_left(
7338
        string $str,
7339
        int $length,
7340
        string $pad_str = ' ',
7341
        string $encoding = 'UTF-8'
7342
    ): string {
7343 7
        return self::str_pad(
7344 7
            $str,
7345 7
            $length,
7346 7
            $pad_str,
7347 7
            \STR_PAD_LEFT,
7348 7
            $encoding
7349
        );
7350
    }
7351
7352
    /**
7353
     * Returns a new string of a given length such that the end of the string
7354
     * is padded. Alias for "UTF8::str_pad()" with a $pad_type of 'right'.
7355
     *
7356
     * @param string $str
7357
     * @param int    $length   <p>Desired string length after padding.</p>
7358
     * @param string $pad_str  [optional] <p>String used to pad, defaults to space. Default: ' '</p>
7359
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7360
     *
7361
     * @psalm-pure
7362
     *
7363
     * @return string
7364
     *                <p>The string with right padding.</p>
7365
     */
7366 7
    public static function str_pad_right(
7367
        string $str,
7368
        int $length,
7369
        string $pad_str = ' ',
7370
        string $encoding = 'UTF-8'
7371
    ): string {
7372 7
        return self::str_pad(
7373 7
            $str,
7374 7
            $length,
7375 7
            $pad_str,
7376 7
            \STR_PAD_RIGHT,
7377 7
            $encoding
7378
        );
7379
    }
7380
7381
    /**
7382
     * Repeat a string.
7383
     *
7384
     * @param string $str        <p>
7385
     *                           The string to be repeated.
7386
     *                           </p>
7387
     * @param int    $multiplier <p>
7388
     *                           Number of time the input string should be
7389
     *                           repeated.
7390
     *                           </p>
7391
     *                           <p>
7392
     *                           multiplier has to be greater than or equal to 0.
7393
     *                           If the multiplier is set to 0, the function
7394
     *                           will return an empty string.
7395
     *                           </p>
7396
     *
7397
     * @psalm-pure
7398
     *
7399
     * @return string
7400
     *                <p>The repeated string.</P>
7401
     */
7402 9
    public static function str_repeat(string $str, int $multiplier): string
7403
    {
7404 9
        $str = self::filter($str);
7405
7406 9
        return \str_repeat($str, $multiplier);
7407
    }
7408
7409
    /**
7410
     * INFO: This is only a wrapper for "str_replace()"  -> the original functions is already UTF-8 safe.
7411
     *
7412
     * Replace all occurrences of the search string with the replacement string
7413
     *
7414
     * @see http://php.net/manual/en/function.str-replace.php
7415
     *
7416
     * @param mixed $search  <p>
7417
     *                       The value being searched for, otherwise known as the needle.
7418
     *                       An array may be used to designate multiple needles.
7419
     *                       </p>
7420
     * @param mixed $replace <p>
7421
     *                       The replacement value that replaces found search
7422
     *                       values. An array may be used to designate multiple replacements.
7423
     *                       </p>
7424
     * @param mixed $subject <p>
7425
     *                       The string or array being searched and replaced on,
7426
     *                       otherwise known as the haystack.
7427
     *                       </p>
7428
     *                       <p>
7429
     *                       If subject is an array, then the search and
7430
     *                       replace is performed with every entry of
7431
     *                       subject, and the return value is an array as
7432
     *                       well.
7433
     *                       </p>
7434
     * @param int   $count   [optional] If passed, this will hold the number of matched and replaced needles
7435
     *
7436
     * @psalm-pure
7437
     *
7438
     * @return mixed this function returns a string or an array with the replaced values
7439
     */
7440 12
    public static function str_replace(
7441
        $search,
7442
        $replace,
7443
        $subject,
7444
        int &$count = null
7445
    ) {
7446
        /**
7447
         * @psalm-suppress PossiblyNullArgument
7448
         */
7449 12
        return \str_replace(
7450 12
            $search,
7451 12
            $replace,
7452 12
            $subject,
7453 12
            $count
7454
        );
7455
    }
7456
7457
    /**
7458
     * Replaces $search from the beginning of string with $replacement.
7459
     *
7460
     * @param string $str         <p>The input string.</p>
7461
     * @param string $search      <p>The string to search for.</p>
7462
     * @param string $replacement <p>The replacement.</p>
7463
     *
7464
     * @psalm-pure
7465
     *
7466
     * @return string
7467
     *                <p>A string after the replacements.</p>
7468
     */
7469 17
    public static function str_replace_beginning(
7470
        string $str,
7471
        string $search,
7472
        string $replacement
7473
    ): string {
7474 17
        if ($str === '') {
7475 4
            if ($replacement === '') {
7476 2
                return '';
7477
            }
7478
7479 2
            if ($search === '') {
7480 2
                return $replacement;
7481
            }
7482
        }
7483
7484 13
        if ($search === '') {
7485 2
            return $str . $replacement;
7486
        }
7487
7488 11
        if (\strpos($str, $search) === 0) {
7489 9
            return $replacement . \substr($str, \strlen($search));
7490
        }
7491
7492 2
        return $str;
7493
    }
7494
7495
    /**
7496
     * Replaces $search from the ending of string with $replacement.
7497
     *
7498
     * @param string $str         <p>The input string.</p>
7499
     * @param string $search      <p>The string to search for.</p>
7500
     * @param string $replacement <p>The replacement.</p>
7501
     *
7502
     * @psalm-pure
7503
     *
7504
     * @return string
7505
     *                <p>A string after the replacements.</p>
7506
     */
7507 17
    public static function str_replace_ending(
7508
        string $str,
7509
        string $search,
7510
        string $replacement
7511
    ): string {
7512 17
        if ($str === '') {
7513 4
            if ($replacement === '') {
7514 2
                return '';
7515
            }
7516
7517 2
            if ($search === '') {
7518 2
                return $replacement;
7519
            }
7520
        }
7521
7522 13
        if ($search === '') {
7523 2
            return $str . $replacement;
7524
        }
7525
7526 11
        if (\strpos($str, $search, \strlen($str) - \strlen($search)) !== false) {
7527 8
            $str = \substr($str, 0, -\strlen($search)) . $replacement;
7528
        }
7529
7530 11
        return $str;
7531
    }
7532
7533
    /**
7534
     * Replace the first "$search"-term with the "$replace"-term.
7535
     *
7536
     * @param string $search
7537
     * @param string $replace
7538
     * @param string $subject
7539
     *
7540
     * @psalm-pure
7541
     *
7542
     * @return string
7543
     *
7544
     * @psalm-suppress InvalidReturnType
7545
     */
7546 2
    public static function str_replace_first(
7547
        string $search,
7548
        string $replace,
7549
        string $subject
7550
    ): string {
7551 2
        $pos = self::strpos($subject, $search);
7552
7553 2
        if ($pos !== false) {
7554
            /**
7555
             * @psalm-suppress InvalidReturnStatement
7556
             */
7557 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...
7558 2
                $subject,
7559 2
                $replace,
7560 2
                $pos,
7561 2
                (int) self::strlen($search)
7562
            );
7563
        }
7564
7565 2
        return $subject;
7566
    }
7567
7568
    /**
7569
     * Replace the last "$search"-term with the "$replace"-term.
7570
     *
7571
     * @param string $search
7572
     * @param string $replace
7573
     * @param string $subject
7574
     *
7575
     * @psalm-pure
7576
     *
7577
     * @return string
7578
     *
7579
     * @psalm-suppress InvalidReturnType
7580
     */
7581 2
    public static function str_replace_last(
7582
        string $search,
7583
        string $replace,
7584
        string $subject
7585
    ): string {
7586 2
        $pos = self::strrpos($subject, $search);
7587 2
        if ($pos !== false) {
7588
            /**
7589
             * @psalm-suppress InvalidReturnStatement
7590
             */
7591 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...
7592 2
                $subject,
7593 2
                $replace,
7594 2
                $pos,
7595 2
                (int) self::strlen($search)
7596
            );
7597
        }
7598
7599 2
        return $subject;
7600
    }
7601
7602
    /**
7603
     * Shuffles all the characters in the string.
7604
     *
7605
     * PS: uses random algorithm which is weak for cryptography purposes
7606
     *
7607
     * @param string $str      <p>The input string</p>
7608
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7609
     *
7610
     * @psalm-pure
7611
     *
7612
     * @return string
7613
     *                <p>The shuffled string.</p>
7614
     */
7615 5
    public static function str_shuffle(string $str, string $encoding = 'UTF-8'): string
7616
    {
7617 5
        if ($encoding === 'UTF-8') {
7618 5
            $indexes = \range(0, (int) \mb_strlen($str) - 1);
7619
            /** @noinspection NonSecureShuffleUsageInspection */
7620 5
            \shuffle($indexes);
7621
7622
            // init
7623 5
            $shuffled_str = '';
7624
7625 5
            foreach ($indexes as &$i) {
7626 5
                $tmp_sub_str = \mb_substr($str, $i, 1);
7627 5
                if ($tmp_sub_str !== false) {
7628 5
                    $shuffled_str .= $tmp_sub_str;
7629
                }
7630
            }
7631
        } else {
7632
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7633
7634
            $indexes = \range(0, (int) self::strlen($str, $encoding) - 1);
7635
            /** @noinspection NonSecureShuffleUsageInspection */
7636
            \shuffle($indexes);
7637
7638
            // init
7639
            $shuffled_str = '';
7640
7641
            foreach ($indexes as &$i) {
7642
                $tmp_sub_str = self::substr($str, $i, 1, $encoding);
7643
                if ($tmp_sub_str !== false) {
7644
                    $shuffled_str .= $tmp_sub_str;
7645
                }
7646
            }
7647
        }
7648
7649 5
        return $shuffled_str;
7650
    }
7651
7652
    /**
7653
     * Returns the substring beginning at $start, and up to, but not including
7654
     * the index specified by $end. If $end is omitted, the function extracts
7655
     * the remaining string. If $end is negative, it is computed from the end
7656
     * of the string.
7657
     *
7658
     * @param string $str
7659
     * @param int    $start    <p>Initial index from which to begin extraction.</p>
7660
     * @param int    $end      [optional] <p>Index at which to end extraction. Default: null</p>
7661
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7662
     *
7663
     * @psalm-pure
7664
     *
7665
     * @return false|string
7666
     *                      <p>The extracted substring.</p><p>If <i>str</i> is shorter than <i>start</i>
7667
     *                      characters long, <b>FALSE</b> will be returned.
7668
     */
7669 18
    public static function str_slice(
7670
        string $str,
7671
        int $start,
7672
        int $end = null,
7673
        string $encoding = 'UTF-8'
7674
    ) {
7675 18
        if ($encoding === 'UTF-8') {
7676 7
            if ($end === null) {
7677 1
                $length = (int) \mb_strlen($str);
7678 6
            } elseif ($end >= 0 && $end <= $start) {
7679 2
                return '';
7680 4
            } elseif ($end < 0) {
7681 1
                $length = (int) \mb_strlen($str) + $end - $start;
7682
            } else {
7683 3
                $length = $end - $start;
7684
            }
7685
7686 5
            return \mb_substr($str, $start, $length);
7687
        }
7688
7689 11
        $encoding = self::normalize_encoding($encoding, 'UTF-8');
7690
7691 11
        if ($end === null) {
7692 5
            $length = (int) self::strlen($str, $encoding);
7693 6
        } elseif ($end >= 0 && $end <= $start) {
7694 2
            return '';
7695 4
        } elseif ($end < 0) {
7696 1
            $length = (int) self::strlen($str, $encoding) + $end - $start;
7697
        } else {
7698 3
            $length = $end - $start;
7699
        }
7700
7701 9
        return self::substr($str, $start, $length, $encoding);
7702
    }
7703
7704
    /**
7705
     * Convert a string to e.g.: "snake_case"
7706
     *
7707
     * @param string $str
7708
     * @param string $encoding [optional] <p>Set the charset for e.g. "mb_" function</p>
7709
     *
7710
     * @psalm-pure
7711
     *
7712
     * @return string
7713
     *                <p>A string in snake_case.</p>
7714
     */
7715 22
    public static function str_snakeize(string $str, string $encoding = 'UTF-8'): string
7716
    {
7717 22
        if ($str === '') {
7718
            return '';
7719
        }
7720
7721 22
        $str = \str_replace(
7722 22
            '-',
7723 22
            '_',
7724 22
            self::normalize_whitespace($str)
7725
        );
7726
7727 22
        if ($encoding !== 'UTF-8' && $encoding !== 'CP850') {
7728 19
            $encoding = self::normalize_encoding($encoding, 'UTF-8');
7729
        }
7730
7731 22
        $str = (string) \preg_replace_callback(
7732 22
            '/([\\p{N}|\\p{Lu}])/u',
7733
            /**
7734
             * @param string[] $matches
7735
             *
7736
             * @psalm-pure
7737
             *
7738
             * @return string
7739
             */
7740
            static function (array $matches) use ($encoding): string {
7741 9
                $match = $matches[1];
7742 9
                $match_int = (int) $match;
7743
7744 9
                if ((string) $match_int === $match) {
7745 4
                    return '_' . $match . '_';
7746
                }
7747
7748 5
                if ($encoding === 'UTF-8') {
7749 5
                    return '_' . \mb_strtolower($match);
7750
                }
7751
7752
                return '_' . self::strtolower($match, $encoding);
7753 22
            },
7754 22
            $str
7755
        );
7756
7757 22
        $str = (string) \preg_replace(
7758
            [
7759 22
                '/\\s+/u',           // convert spaces to "_"
7760
                '/^\\s+|\\s+$/u', // trim leading & trailing spaces
7761
                '/_+/',                 // remove double "_"
7762
            ],
7763
            [
7764 22
                '_',
7765
                '',
7766
                '_',
7767
            ],
7768 22
            $str
7769
        );
7770
7771 22
        return \trim(\trim($str, '_')); // trim leading & trailing "_" + whitespace
7772
    }
7773
7774
    /**
7775
     * Sort all characters according to code points.
7776
     *
7777
     * @param string $str    <p>A UTF-8 string.</p>
7778
     * @param bool   $unique <p>Sort unique. If <strong>true</strong>, repeated characters are ignored.</p>
7779
     * @param bool   $desc   <p>If <strong>true</strong>, will sort characters in reverse code point order.</p>
7780
     *
7781
     * @psalm-pure
7782
     *
7783
     * @return string
7784
     *                <p>A string of sorted characters.</p>
7785
     */
7786 2
    public static function str_sort(string $str, bool $unique = false, bool $desc = false): string
7787
    {
7788 2
        $array = self::codepoints($str);
7789
7790 2
        if ($unique) {
7791 2
            $array = \array_flip(\array_flip($array));
7792
        }
7793
7794 2
        if ($desc) {
7795 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

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

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

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

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