Symfony\Polyfill\Mbstring
Mbstring{} │ final │ CF7 1.0
Partial mbstring implementation in PHP, iconv based, UTF-8 centric.
Implemented:
- mb_chr - Returns a specific character from its Unicode code point
- mb_convert_encoding - Convert character encoding
- mb_convert_variables - Convert character code in variable(s)
- mb_decode_mimeheader - Decode string in MIME header field
- mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED
- mb_decode_numericentity - Decode HTML numeric string reference to character
- mb_encode_numericentity - Encode character to HTML numeric string reference
- mb_convert_case - Perform case folding on a string
- mb_detect_encoding - Detect character encoding
- mb_get_info - Get internal settings of mbstring
- mb_http_input - Detect HTTP input character encoding
- mb_http_output - Set/Get HTTP output character encoding
- mb_internal_encoding - Set/Get internal character encoding
- mb_list_encodings - Returns an array of all supported encodings
- mb_ord - Returns the Unicode code point of a character
- mb_output_handler - Callback function converts character encoding in output buffer
- mb_scrub - Replaces ill-formed byte sequences with substitute characters
- mb_strlen - Get string length
- mb_strpos - Find position of first occurrence of string in a string
- mb_strrpos - Find position of last occurrence of a string in a string
- mb_str_split - Convert a string to an array
- mb_strtolower - Make a string lowercase
- mb_strtoupper - Make a string uppercase
- mb_substitute_character - Set/Get substitution character
- mb_substr - Get part of string
- mb_stripos - Finds position of first occurrence of a string within another, case insensitive
- mb_stristr - Finds first occurrence of a string within another, case insensitive
- mb_strrchr - Finds the last occurrence of a character in a string within another
- mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive
- mb_strripos - Finds position of last occurrence of a string within another, case insensitive
- mb_strstr - Finds first occurrence of a string within another
- mb_strwidth - Return width of string
- mb_substr_count - Count the number of substring occurrences
- mb_ucfirst - Make a string's first character uppercase
- mb_lcfirst - Make a string's first character lowercase
- mb_trim - Strip whitespace (or other characters) from the beginning and end of a string
- mb_ltrim - Strip whitespace (or other characters) from the beginning of a string
- mb_rtrim - Strip whitespace (or other characters) from the end of a string
Not implemented:
- mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more)
- mb_ereg_* - Regular expression with multibyte support
- mb_parse_str - Parse GET/POST/COOKIE data and set global variable
- mb_preferred_mime_name - Get MIME charset string
- mb_regex_encoding - Returns current encoding for multibyte regex as string
- mb_regex_set_options - Set/Get the default options for mbregex functions
- mb_send_mail - Send encoded mail
- mb_split - Split multibyte string using regular expression
- mb_strcut - Get part of string
- mb_strimwidth - Get truncated string with specified width
Хуков нет.
Использование
$Mbstring = new Mbstring(); // use class methods
Методы
- public static mb_check_encoding($var = null, $encoding = null)
- public static mb_chr($code, $encoding = null)
- public static mb_convert_case($s, $mode, $encoding = null)
- public static mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
- public static mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
- public static mb_decode_mimeheader($s)
- public static mb_decode_numericentity($s, $convmap, $encoding = null)
- public static mb_detect_encoding($str, $encodingList = null, $strict = false)
- public static mb_detect_order($encodingList = null)
- public static mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
- public static mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
- public static mb_encoding_aliases($encoding)
- public static mb_get_info($type = 'all')
- public static mb_http_input($type = '')
- public static mb_http_output($encoding = null)
- public static mb_internal_encoding($encoding = null)
- public static mb_language($lang = null)
- public static mb_lcfirst(string $string, ?string $encoding = null)
- public static mb_list_encodings()
- public static mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null)
- public static mb_ord($s, $encoding = null)
- public static mb_output_handler($contents, $status)
- public static mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null)
- public static mb_scrub(?string $string, ?string $encoding = null)
- public static mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
- public static mb_str_split($string, $split_length = 1, $encoding = null)
- public static mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
- public static mb_stristr($haystack, $needle, $part = false, $encoding = null)
- public static mb_strlen($s, $encoding = null)
- public static mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
- public static mb_strrchr($haystack, $needle, $part = false, $encoding = null)
- public static mb_strrichr($haystack, $needle, $part = false, $encoding = null)
- public static mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
- public static mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
- public static mb_strstr($haystack, $needle, $part = false, $encoding = null)
- public static mb_strtolower($s, $encoding = null)
- public static mb_strtoupper($s, $encoding = null)
- public static mb_strwidth($s, $encoding = null)
- public static mb_substitute_character($c = null)
- public static mb_substr($s, $start, $length = null, $encoding = null)
- public static mb_substr_count($haystack, $needle, $encoding = null)
- public static mb_trim(string $string, ?string $characters = null, ?string $encoding = null)
- public static mb_ucfirst(string $string, ?string $encoding = null)
- private static assertCodepointEncoding(string $encoding, string $function)
- private static assertEncoding(string $encoding, string $errorFormat)
- private static convertArray(array $array, $toEncoding, $fromEncoding, array $referenceIds = [])
- private static find(string $function, $haystack, $needle, $offset, $encoding, bool $reverse, bool $fold)
- private static findPart(string $function, $haystack, $needle, $part, $encoding, bool $reverse, bool $fold)
- private static foldCase(string $s)
- private static getConversionError(string $function, int $argument, $toEncoding, $fromEncoding)
- private static getData($file)
- private static getEncoding($encoding)
- private static html_encoding_callback(array $m)
- private static iconv($fromEncoding, $toEncoding, $s)
- private static isConvertibleEncoding(string $encoding)
- private static markIllFormedUtf8(string $s)
- private static mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function)
- private static mb_trim_invalid_utf8(string $string, ?string $characters, string $function)
- private static prepareSearch(string $haystack, string $needle, string $encoding, bool $fold, bool $markIllFormed)
- private static scrubUtf8(string $s)
- private static searchEncoding($encoding, bool $fold)
- private static title_case(array $s)
- private static utf8(string $s, string $encoding, &$dropped = 0)
- private static utf8Length(string $s)
- private static utf8Offset(string $s, int $offset, bool $bytes)
Код Mbstring{} Mbstring{} CF7 6.2.4.1
final class Mbstring
{
public const MB_CASE_FOLD = \PHP_INT_MAX;
private const SIMPLE_CASE_FOLD = [
['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'],
];
// Encodings rejected by mb_ord() and mb_chr(), with their aliases
private const UNSUPPORTED_CODEPOINT_ENCODINGS = [
'BASE64' => 'BASE64',
'UUENCODE' => 'UUENCODE',
'X-UUENCODE' => 'UUENCODE',
'HTML-ENTITIES' => 'HTML-ENTITIES',
'HTML' => 'HTML-ENTITIES',
'QUOTED-PRINTABLE' => 'Quoted-Printable',
'QPRINT' => 'Quoted-Printable',
'UTF-7' => 'UTF-7',
'UTF7' => 'UTF-7',
'UTF7-IMAP' => 'UTF7-IMAP',
'MUTF-7' => 'UTF7-IMAP',
'JIS' => 'JIS',
'ISO-2022-JP' => 'ISO-2022-JP',
'ISO-2022-JP-2004' => 'ISO-2022-JP-2004',
'ISO-2022-JP-MOBILE#KDDI' => 'ISO-2022-JP-MOBILE#KDDI',
'ISO-2022-JP-KDDI' => 'ISO-2022-JP-MOBILE#KDDI',
'ISO-2022-JP-MS' => 'ISO-2022-JP-MS',
'ISO2022JPMS' => 'ISO-2022-JP-MS',
'CP50220' => 'CP50220',
'CP50220RAW' => 'CP50220',
'CP50220-RAW' => 'CP50220',
'JIS-MS' => 'CP50220',
'CP50221' => 'CP50221',
'CP50222' => 'CP50222',
];
// Well-formed characters in group 1, then the maximal subparts of ill-formed sequences
private const UTF8_CHAR_REGEX = '/([\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})|\xE0[\xA0-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]|\xED[\x80-\x9F]|\xF0[\x90-\xBF][\x80-\xBF]?|[\xF1-\xF3][\x80-\xBF]{1,2}|\xF4[\x80-\x8F][\x80-\xBF]?|[\x80-\xFF]/';
private static $encodingList = ['ASCII', 'UTF-8'];
private static $language = 'neutral';
private static $internalEncoding = 'UTF-8';
private static $iconvSupportsIgnore;
private static $convertibleEncodings = ['UTF-8' => true];
public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
{
if (\is_string($fromEncoding) && 2 < \strlen($fromEncoding) && '"' === $fromEncoding[0] && '"' === $fromEncoding[-1]) {
$fromEncoding = substr($fromEncoding, 1, -1);
}
if (80000 <= \PHP_VERSION_ID
&& (!isset(self::$convertibleEncodings[$toEncoding]) || (null !== $fromEncoding && !(\is_string($fromEncoding) && isset(self::$convertibleEncodings[$fromEncoding]))))
&& null !== $error = self::getConversionError('mb_convert_encoding', 2, $toEncoding, $fromEncoding)
) {
throw new \ValueError($error);
}
if (\is_array($s)) {
return self::convertArray($s, $toEncoding, $fromEncoding);
}
if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) {
$fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
} else {
$fromEncoding = self::getEncoding($fromEncoding);
}
$toEncoding = self::getEncoding($toEncoding);
if ('BASE64' === $fromEncoding) {
$s = base64_decode($s);
$fromEncoding = $toEncoding;
}
if ('BASE64' === $toEncoding) {
return base64_encode($s);
}
if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) {
if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) {
$fromEncoding = 'Windows-1252';
}
if ('UTF-8' !== $fromEncoding) {
$s = self::iconv($fromEncoding, 'UTF-8', $s);
} elseif (!preg_match('//u', $s)) {
$s = self::scrubUtf8($s);
}
return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s);
}
if ('HTML-ENTITIES' === $fromEncoding) {
$decodeControlChars = static function ($m) {
$code = '' !== ($m[2] ?? '') ? hexdec($m[2]) : (int) $m[1];
if ($code < 32 || 127 === $code) {
return \chr($code);
}
if (128 <= $code && $code <= 159) {
return "\xC2".\chr(0x80 | ($code & 0x3F));
}
return $m[0];
};
if (\PHP_VERSION_ID >= 70400) {
$s = html_entity_decode($s, \ENT_QUOTES, 'UTF-8');
// html_entity_decode() leaves numeric entities for C0/C1 control
// characters as-is (HTML spec), but mb_convert_encoding() decodes
// them. Catch what html_entity_decode() missed.
if (false !== strpos($s, '&#')) {
$s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
}
} else {
// PHP < 7.4: html_entity_decode() truncates strings at NUL bytes,
// so decode the control character entities first then call
// html_entity_decode() on each NUL-delimited chunk independently.
$s = preg_replace_callback('/&#(?:0*([0-9]++)|[xX]0*([0-9a-fA-F]++));/', $decodeControlChars, $s);
$s = implode("\0", array_map(static function ($chunk) {
return html_entity_decode($chunk, \ENT_QUOTES, 'UTF-8');
}, explode("\0", $s)));
}
$fromEncoding = 'UTF-8';
}
if ('UTF-8' === $fromEncoding && !preg_match('//u', $s)) {
$s = self::scrubUtf8($s);
}
return self::iconv($fromEncoding, $toEncoding, $s);
}
public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
{
if (\is_string($fromEncoding) && 2 < \strlen($fromEncoding) && '"' === $fromEncoding[0] && '"' === $fromEncoding[-1]) {
$fromEncoding = substr($fromEncoding, 1, -1);
}
if (80000 <= \PHP_VERSION_ID && null !== $error = self::getConversionError('mb_convert_variables', 1, $toEncoding, $fromEncoding)) {
throw new \ValueError($error);
}
$ok = true;
foreach ($vars as $i => &$var) {
$convert = static function (&$v) use (&$ok, $toEncoding, $fromEncoding, $i) {
if (\is_string($v)) {
if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
$ok = false;
}
} elseif (80600 <= \PHP_VERSION_ID && !\is_object($v)) {
trigger_error(\sprintf('mb_convert_variables(): Argument #%d must be of type string|array|object or only contain entries of type string|array|object, %s given', 3 + $i, strtok(get_debug_type($v), ' ')), \E_USER_WARNING);
}
};
if (\is_array($var)) {
array_walk_recursive($var, $convert);
} else {
$convert($var);
}
}
unset($var);
return $ok ? $fromEncoding : false;
}
public static function mb_decode_mimeheader($s)
{
return iconv_mime_decode($s, 2, self::$internalEncoding);
}
public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
{
trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING);
}
public static function mb_decode_numericentity($s, $convmap, $encoding = null)
{
if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
return null;
}
if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
return false;
}
if (null !== $encoding && !\is_scalar($encoding)) {
trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
return ''; // Instead of null (cf. mb_encode_numericentity).
}
$s = (string) $s;
if ('' === $s) {
return '';
}
$encoding = self::getEncoding($encoding);
if ('UTF-8' === $encoding) {
$encoding = null;
if (!preg_match('//u', $s)) {
$s = self::scrubUtf8($s);
}
} else {
$s = self::iconv($encoding, 'UTF-8', $s);
}
$cnt = floor(\count($convmap) / 4) * 4;
for ($i = 0; $i < $cnt; $i += 4) {
// collector_decode_htmlnumericentity ignores $convmap[$i + 3]
$convmap[$i] += $convmap[$i + 2];
$convmap[$i + 1] += $convmap[$i + 2];
}
$s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))'.(\PHP_VERSION_ID >= 80200 ? '' : '(?!&)').';?/', static function (array $m) use ($cnt, $convmap) {
$c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1];
for ($i = 0; $i < $cnt; $i += 4) {
if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) {
return self::mb_chr($c - $convmap[$i + 2]);
}
}
return $m[0];
}, $s);
if (null === $encoding) {
return $s;
}
return self::iconv('UTF-8', $encoding, $s);
}
public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
{
if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
return null;
}
if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
return false;
}
if (null !== $encoding && !\is_scalar($encoding)) {
trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
return null; // Instead of '' (cf. mb_decode_numericentity).
}
if (null !== $is_hex && !\is_scalar($is_hex)) {
trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING);
return null;
}
$s = (string) $s;
if ('' === $s) {
return '';
}
$encoding = self::getEncoding($encoding);
if ('UTF-8' === $encoding) {
$encoding = null;
if (!preg_match('//u', $s)) {
$s = self::scrubUtf8($s);
}
} else {
$s = self::iconv($encoding, 'UTF-8', $s);
}
static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
$cnt = floor(\count($convmap) / 4) * 4;
$i = 0;
$len = \strlen($s);
$result = '';
while ($i < $len) {
$ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"] ?? 1;
$uchr = substr($s, $i, $ulen);
$i += $ulen;
// code points above U+10FFFF are outside any convmap, but mb_ord() rejects them
if (false === $c = self::mb_ord($uchr)) {
$result .= $uchr;
continue;
}
for ($j = 0; $j < $cnt; $j += 4) {
if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) {
$cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3];
$result .= $is_hex ? \sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';';
continue 2;
}
}
$result .= $uchr;
}
if (null === $encoding) {
return $result;
}
return self::iconv('UTF-8', $encoding, $result);
}
public static function mb_convert_case($s, $mode, $encoding = null)
{
$s = (string) $s;
if ('' === $s) {
return '';
}
$encoding = self::getEncoding($encoding);
if ('UTF-8' === $encoding) {
$encoding = null;
if (!preg_match('//u', $s)) {
$s = self::scrubUtf8($s);
}
} else {
$s = self::iconv($encoding, 'UTF-8', $s);
}
if (\MB_CASE_TITLE == $mode) {
static $titleRegexp = null;
if (null === $titleRegexp) {
$titleRegexp = self::getData('titleCaseRegexp');
}
$s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s);
} else {
if (\MB_CASE_UPPER == $mode) {
static $upper = null;
if (null === $upper) {
$upper = self::getData('upperCase');
}
$map = $upper;
} else {
if (self::MB_CASE_FOLD === $mode) {
static $caseFolding = null;
if (null === $caseFolding) {
$caseFolding = self::getData('caseFolding');
}
$s = strtr($s, $caseFolding);
}
static $lower = null;
if (null === $lower) {
$lower = self::getData('lowerCase');
}
$map = $lower;
}
static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
$i = 0;
$len = \strlen($s);
while ($i < $len) {
$ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"] ?? 1;
$uchr = substr($s, $i, $ulen);
$i += $ulen;
if (isset($map[$uchr])) {
$uchr = $map[$uchr];
$nlen = \strlen($uchr);
if ($nlen == $ulen) {
$nlen = $i;
do {
$s[--$nlen] = $uchr[--$ulen];
} while ($ulen);
} else {
$s = substr_replace($s, $uchr, $i - $ulen, $ulen);
$len += $nlen - $ulen;
$i += $nlen - $ulen;
}
}
}
}
if (null === $encoding) {
return $s;
}
return self::iconv('UTF-8', $encoding, $s);
}
public static function mb_internal_encoding($encoding = null)
{
if (null === $encoding) {
return self::$internalEncoding;
}
$normalizedEncoding = self::getEncoding($encoding);
if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) {
self::$internalEncoding = $normalizedEncoding;
return true;
}
if (80000 > \PHP_VERSION_ID) {
return false;
}
throw new \ValueError(\sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
}
public static function mb_language($lang = null)
{
if (null === $lang) {
return self::$language;
}
switch ($normalizedLang = strtolower($lang)) {
case 'uni':
case 'neutral':
self::$language = $normalizedLang;
return true;
}
if (80000 > \PHP_VERSION_ID) {
return false;
}
throw new \ValueError(\sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
}
public static function mb_list_encodings()
{
return ['UTF-8'];
}
public static function mb_encoding_aliases($encoding)
{
switch (strtoupper($encoding)) {
case 'UTF8':
case 'UTF-8':
return ['utf8'];
}
return false;
}
public static function mb_check_encoding($var = null, $encoding = null)
{
if (null === $encoding) {
if (null === $var) {
return false;
}
$encoding = self::$internalEncoding;
}
if (!\is_array($var)) {
if ('UTF-8' === self::getEncoding($encoding)) {
return (bool) preg_match('//u', $var);
}
return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var);
}
foreach ($var as $key => $value) {
if (!self::mb_check_encoding($key, $encoding)) {
return false;
}
if (!self::mb_check_encoding($value, $encoding)) {
return false;
}
}
return true;
}
public static function mb_detect_encoding($str, $encodingList = null, $strict = false)
{
if (null === $encodingList) {
$encodingList = self::$encodingList;
} else {
if (!\is_array($encodingList)) {
$encodingList = array_map('trim', explode(',', $encodingList));
}
$encodingList = array_map('strtoupper', $encodingList);
}
foreach ($encodingList as $enc) {
switch ($enc) {
case 'ASCII':
if (!preg_match('/[\x80-\xFF]/', $str)) {
return $enc;
}
break;
case 'UTF8':
case 'UTF-8':
if (preg_match('//u', $str)) {
return 'UTF-8';
}
break;
default:
if (0 === strncmp($enc, 'ISO-8859-', 9)) {
return $enc;
}
}
}
return false;
}
public static function mb_detect_order($encodingList = null)
{
if (null === $encodingList) {
return self::$encodingList;
}
if (!\is_array($encodingList)) {
$encodingList = array_map('trim', explode(',', $encodingList));
}
$encodingList = array_map('strtoupper', $encodingList);
foreach ($encodingList as $enc) {
switch ($enc) {
default:
if (strncmp($enc, 'ISO-8859-', 9)) {
return false;
}
// no break
case 'ASCII':
case 'UTF8':
case 'UTF-8':
}
}
self::$encodingList = $encodingList;
return true;
}
public static function mb_strlen($s, $encoding = null)
{
$encoding = self::getEncoding($encoding);
if ('CP850' === $encoding || 'ASCII' === $encoding) {
return \strlen($s);
}
if (false !== $len = @iconv_strlen($s, $encoding)) {
return $len;
}
if ('UTF-8' !== $encoding) {
return $len;
}
return preg_match_all('/[\x00-\x7F]|[\xC0-\xDF][\x80-\xBF]?|[\xE0-\xEF][\x80-\xBF]{0,2}|[\xF0-\xF7][\x80-\xBF]{0,3}|[\xF8-\xFB][\x80-\xBF]{0,4}|[\xFC-\xFD][\x80-\xBF]{0,5}|[\x80-\xBF\xFE\xFF]/s', $s);
}
public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
{
return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, false, false);
}
public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
{
if ($offset != (int) $offset) {
$offset = 0;
}
return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, true, false);
}
public static function mb_str_split($string, $split_length = 1, $encoding = null)
{
if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING);
return null;
}
if (1 > $split_length = (int) $split_length) {
if (80000 > \PHP_VERSION_ID) {
trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);
return false;
}
throw new \ValueError('Argument #2 ($length) must be greater than 0');
}
if (80300 <= \PHP_VERSION_ID && 0x3FFFFFFF < $split_length) {
throw new \ValueError('Argument #2 ($length) is too large');
}
if (null === $encoding) {
$encoding = mb_internal_encoding();
}
if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
$string = (string) $string;
if (\strlen($string) <= $split_length) {
return '' === $string ? [] : [$string];
}
$rx = '/(';
while (65535 < $split_length) {
$rx .= '.{65535}';
$split_length -= 65535;
}
$rx .= '.{'.$split_length.'})/Aus';
return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
}
$result = [];
$length = mb_strlen($string, $encoding);
for ($i = 0; $i < $length; $i += $split_length) {
$result[] = mb_substr($string, $i, $split_length, $encoding);
}
return $result;
}
public static function mb_strtolower($s, $encoding = null)
{
return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding);
}
public static function mb_strtoupper($s, $encoding = null)
{
return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding);
}
public static function mb_substitute_character($c = null)
{
if (null === $c) {
return 'none';
}
if (0 === strcasecmp($c, 'none')) {
return true;
}
if (80000 > \PHP_VERSION_ID) {
return false;
}
if (\is_int($c) || 'long' === $c || 'entity' === $c) {
return false;
}
throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint');
}
public static function mb_substr($s, $start, $length = null, $encoding = null)
{
$encoding = self::getEncoding($encoding);
if ('CP850' === $encoding || 'ASCII' === $encoding) {
return (string) substr($s, $start, null === $length ? 2147483647 : $length);
}
if ($start < 0) {
$start = iconv_strlen($s, $encoding) + $start;
if ($start < 0) {
$start = 0;
}
}
if (null === $length) {
$length = 2147483647;
} elseif ($length < 0) {
$length = iconv_strlen($s, $encoding) + $length - $start;
if ($length < 0) {
return '';
}
}
return (string) iconv_substr($s, $start, $length, $encoding);
}
public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
{
return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, false, true);
}
public static function mb_stristr($haystack, $needle, $part = false, $encoding = null)
{
return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, false, true);
}
public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null)
{
return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, true, false);
}
public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null)
{
return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, true, true);
}
public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
{
return self::find(__FUNCTION__, $haystack, $needle, $offset, $encoding, true, true);
}
public static function mb_strstr($haystack, $needle, $part = false, $encoding = null)
{
return self::findPart(__FUNCTION__, $haystack, $needle, $part, $encoding, false, false);
}
public static function mb_get_info($type = 'all')
{
$info = [
'internal_encoding' => self::$internalEncoding,
'http_output' => 'pass',
'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)',
'func_overload' => 0,
'func_overload_list' => 'no overload',
'mail_charset' => 'UTF-8',
'mail_header_encoding' => 'BASE64',
'mail_body_encoding' => 'BASE64',
'illegal_chars' => 0,
'encoding_translation' => 'Off',
'language' => self::$language,
'detect_order' => self::$encodingList,
'substitute_character' => 'none',
'strict_detection' => 'Off',
];
if ('all' === $type) {
return $info;
}
if (isset($info[$type])) {
return $info[$type];
}
return false;
}
public static function mb_http_input($type = '')
{
return false;
}
public static function mb_http_output($encoding = null)
{
return null !== $encoding ? 'pass' === $encoding : 'pass';
}
public static function mb_strwidth($s, $encoding = null)
{
$encoding = self::getEncoding($encoding);
static $wideChars = null;
if (null === $wideChars) {
$wideChars = self::getData('eastAsianWidthRegexp');
}
if ('UTF-8' !== $encoding) {
$s = self::iconv($encoding, 'UTF-8', $s);
} elseif (!preg_match('//u', $s)) {
// Each maximal subpart of an ill-formed sequence is one column wide
return preg_match_all(self::UTF8_CHAR_REGEX, $s) + preg_match_all($wideChars, self::scrubUtf8($s));
}
$s = preg_replace($wideChars, '', $s, -1, $wide);
return ($wide << 1) + iconv_strlen($s, 'UTF-8');
}
public static function mb_substr_count($haystack, $needle, $encoding = null)
{
if ('' === $needle = (string) $needle) {
if (80000 > \PHP_VERSION_ID) {
trigger_error('mb_substr_count(): Empty substring', \E_USER_WARNING);
return false;
}
throw new \ValueError('mb_substr_count(): Argument #2 ($needle) must not be empty');
}
if (!$search = self::prepareSearch((string) $haystack, $needle, self::getEncoding($encoding), false, true)) {
return 0;
}
return substr_count($search[0], $search[1]);
}
public static function mb_output_handler($contents, $status)
{
return $contents;
}
public static function mb_chr($code, $encoding = null)
{
if (null !== $encoding && !self::assertCodepointEncoding($encoding, 'mb_chr')) {
return false;
}
if (0x80 > $code %= 0x200000) {
$s = \chr($code);
} elseif (0x800 > $code) {
$s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
} elseif (0x10000 > $code) {
$s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
} else {
$s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
}
if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
$s = mb_convert_encoding($s, $encoding, 'UTF-8');
}
return $s;
}
public static function mb_ord($s, $encoding = null)
{
if ('' === (string) $s) {
if (80000 > \PHP_VERSION_ID) {
trigger_error('mb_ord(): Empty string', \E_USER_WARNING);
return false;
}
throw new \ValueError('mb_ord(): Argument #1 ($string) must not be empty');
}
if (null !== $encoding && !self::assertCodepointEncoding($encoding, 'mb_ord')) {
return false;
}
if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
$s = mb_convert_encoding($s, 'UTF-8', $encoding);
}
$s = unpack('C*', substr($s, 0, 4));
$code = $s[1] ?? 0;
if (0x80 > $code) {
return $code;
}
if (0xC2 > $code || 0xF4 < $code) {
return false;
}
$c2 = $s[2] ?? 0;
if (0x80 !== ($c2 & 0xC0)) {
return false;
}
if (0xE0 > $code) {
return (($code & 0x1F) << 6) | ($c2 & 0x3F);
}
$c3 = $s[3] ?? 0;
if (0x80 !== ($c3 & 0xC0)
|| 0xE0 === $code && 0xA0 > $c2
|| 0xED === $code && 0xA0 <= $c2
) {
return false;
}
if (0xF0 > $code) {
return (($code & 0x0F) << 12) | (($c2 & 0x3F) << 6) | ($c3 & 0x3F);
}
$c4 = $s[4] ?? 0;
if (0x80 !== ($c4 & 0xC0)
|| 0xF0 === $code && 0x90 > $c2
|| 0xF4 === $code && 0x90 <= $c2
) {
return false;
}
return (($code & 0x07) << 18) | (($c2 & 0x3F) << 12) | (($c3 & 0x3F) << 6) | ($c4 & 0x3F);
}
/** @return string|false */
public static function mb_scrub(?string $string, ?string $encoding = null): string
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} elseif (!self::assertEncoding($encoding, 'mb_scrub(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
return self::mb_convert_encoding((string) $string, $encoding, $encoding);
}
/** @return string|false */
public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} elseif (!self::assertEncoding($encoding, 'mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
if (0 >= $padStringLength = self::mb_strlen($pad_string, $encoding)) {
if (\PHP_VERSION_ID < 80000) {
trigger_error('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string', \E_USER_WARNING);
return false;
}
throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
}
if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
if (\PHP_VERSION_ID < 80000) {
trigger_error('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH', \E_USER_WARNING);
return false;
}
throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
}
$paddingRequired = $length - self::mb_strlen($string, $encoding);
if ($paddingRequired < 1) {
return $string;
}
switch ($pad_type) {
case \STR_PAD_LEFT:
$leftPaddingLength = $paddingRequired;
break;
case \STR_PAD_RIGHT:
$leftPaddingLength = 0;
break;
default:
$leftPaddingLength = intdiv($paddingRequired, 2);
}
$rightPaddingLength = $paddingRequired - $leftPaddingLength;
return str_repeat($pad_string, intdiv($leftPaddingLength, $padStringLength)).self::mb_substr($pad_string, 0, $leftPaddingLength % $padStringLength, $encoding)
.$string
.str_repeat($pad_string, intdiv($rightPaddingLength, $padStringLength)).self::mb_substr($pad_string, 0, $rightPaddingLength % $padStringLength, $encoding);
}
/** @return string|false */
public static function mb_ucfirst(string $string, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} elseif (!self::assertEncoding($encoding, 'mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
$firstChar = mb_substr($string, 0, 1, $encoding);
$firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding);
return $firstChar.mb_substr($string, 1, null, $encoding);
}
/** @return string|false */
public static function mb_lcfirst(string $string, ?string $encoding = null)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} elseif (!self::assertEncoding($encoding, 'mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
$firstChar = mb_substr($string, 0, 1, $encoding);
$firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding);
return $firstChar.mb_substr($string, 1, null, $encoding);
}
/** @return string|false */
public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
}
/** @return string|false */
public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__);
}
/** @return string|false */
public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null)
{
return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__);
}
/**
* Finds the position of $needle in $haystack like native mbstring.
*
* @return int|false
*/
private static function find(string $function, $haystack, $needle, $offset, $encoding, bool $reverse, bool $fold)
{
$needle = (string) $needle;
if (80000 > \PHP_VERSION_ID && '' === $needle && 'mb_stripos' === $function) {
trigger_error($function.'(): Empty delimiter', \E_USER_WARNING);
return false;
}
if (!$search = self::prepareSearch((string) $haystack, $needle, self::searchEncoding($encoding, $fold), $fold, $fold)) {
return false;
}
[$haystack, $needle, $bytes, $dropped] = $search;
if (80000 > \PHP_VERSION_ID && $fold && ('' === $haystack || '' === $needle)) {
return false;
}
if (false === $start = self::utf8Offset($haystack, $offset = (int) $offset, $bytes)) {
// Native mbstring counts the ill-formed sequences that iconv() dropped: the offset could be in range
if ($dropped && abs($offset) <= self::utf8Length($haystack) + $dropped) {
return false;
}
if (80000 > \PHP_VERSION_ID) {
trigger_error($function.'(): '.($reverse ? 'Offset is greater than the length of haystack string' : 'Offset not contained in string'), \E_USER_WARNING);
return false;
}
throw new \ValueError($function.'(): Argument #3 ($offset) must be contained in argument #1 ($haystack)');
}
if (80000 > \PHP_VERSION_ID && '' === $needle) {
if ('mb_strpos' === $function) {
trigger_error($function.'(): Empty delimiter', \E_USER_WARNING);
}
return false;
}
if (!$reverse) {
$pos = strpos($haystack, $needle, $start);
} else {
$pos = strrpos($haystack, $needle, 0 > $offset ? $start - \strlen($haystack) : $start);
}
return false === $pos || $bytes ? $pos : self::utf8Length(substr($haystack, 0, $pos));
}
/**
* Returns the part of $haystack before or from $needle like native mbstring.
*
* @return string|false
*/
private static function findPart(string $function, $haystack, $needle, $part, $encoding, bool $reverse, bool $fold)
{
$needle = (string) $needle;
if (80000 > \PHP_VERSION_ID && '' === $needle) {
if (!$reverse) {
trigger_error($function.'(): Empty delimiter', \E_USER_WARNING);
}
return false;
}
$haystack = (string) $haystack;
if (!$search = self::prepareSearch($haystack, $needle, $encoding = self::searchEncoding($encoding, $fold), $fold, $fold)) {
return false;
}
[$h, $n, $bytes] = $search;
if (false === $pos = $reverse ? strrpos($h, $n) : strpos($h, $n)) {
return false;
}
if ($bytes) {
return (string) ($part ? substr($haystack, 0, $pos) : substr($haystack, $pos));
}
if ($fold || ('UTF-8' === $encoding && (!preg_match('//u', $haystack) || !preg_match('//u', $needle)))) {
// Cut after as many characters as native mbstring counts before the match
$pos = self::utf8Length(substr($h, 0, $pos));
$h = 'UTF-8' === $encoding ? self::markIllFormedUtf8($haystack) : self::utf8($haystack, $encoding);
$pos = self::utf8Offset($h, $pos, false);
}
$h = (string) ($part ? substr($h, 0, $pos) : substr($h, $pos));
if (false !== strpos($h, "\xFF")) {
$h = str_replace("\xFF", 'none' === mb_substitute_character() ? '' : '?', $h);
}
return 'UTF-8' === $encoding ? $h : (string) @self::iconv('UTF-8', $encoding, $h);
}
/**
* @return array{string, string, bool, int}|null The haystack and the needle to search, whether offsets in them count bytes rather than UTF-8 characters, and how many bytes of ill-formed sequences iconv() dropped from the haystack
*/
private static function prepareSearch(string $haystack, string $needle, string $encoding, bool $fold, bool $markIllFormed)
{
if ('ASCII' === $encoding || ('CP850' === $encoding && !$fold)) {
if ('ASCII' === $encoding) {
// Native mbstring turns every byte above 0x7F into the same error marker
$haystack = preg_replace('/[\x80-\xFF]/', "\xFF", $haystack);
$needle = preg_replace('/[\x80-\xFF]/', "\xFF", $needle);
}
if ($fold) {
$haystack = strtr($haystack, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
$needle = strtr($needle, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
}
return [$haystack, $needle, true, 0];
}
$dropped = 0;
if ('UTF-8' !== $encoding) {
if (false === ($haystack = self::utf8($haystack, $encoding, $dropped)) || false === $needle = self::utf8($needle, $encoding)) {
return null;
}
} elseif ($markIllFormed) {
$haystack = self::markIllFormedUtf8($haystack);
$needle = self::markIllFormedUtf8($needle);
}
return $fold ? [self::foldCase($haystack), self::foldCase($needle), false, $dropped] : [$haystack, $needle, false, $dropped];
}
private static function searchEncoding($encoding, bool $fold): string
{
$normalizedEncoding = self::getEncoding($encoding);
// Native mbstring folds the case of 8bit strings as Latin-1
return $fold && 'CP850' === $normalizedEncoding && null !== $encoding && 'CP850' !== strtoupper($encoding) ? 'ISO-8859-1' : $normalizedEncoding;
}
/**
* Converts $s to UTF-8 to search it, ending with "\xFF", a byte that is invalid in UTF-8, when its last sequence is truncated, like native mbstring.
*
* @param int $dropped Set to the number of bytes of the ill-formed sequences that iconv() dropped elsewhere
*
* @return string|false False when the encoding is unknown
*/
private static function utf8(string $s, string $encoding, &$dropped = 0)
{
if ('UTF-8' === $encoding || '' === $s) {
return $s;
}
if (false !== $u = @iconv($encoding, 'UTF-8', $s)) {
return $u;
}
// Let iconv() warn about an unknown encoding
if (false === iconv($encoding, 'UTF-8', '')) {
return false;
}
// iconv() drops the ill-formed sequences, but fails on a truncated one at the end
for ($i = 0; $i < 4; ++$i) {
if (false !== $u = @self::iconv($encoding, 'UTF-8', $i ? substr($s, 0, -$i) : $s)) {
$dropped = \strlen($s) - $i - \strlen((string) @iconv('UTF-8', $encoding, $u));
return $i || '' === $u ? $u."\xFF" : $u;
}
}
$dropped = \strlen($s);
return "\xFF";
}
/**
* Replaces each maximal subpart of ill-formed UTF-8 sequences with "\xFF", like native mbstring before searching case-insensitively.
*/
private static function markIllFormedUtf8(string $s): string
{
if (preg_match('//u', $s)) {
return $s;
}
return preg_replace('/\G(?:[\x00-\x7F]|[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2})*+\K(?:\xE0[\xA0-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]|\xED[\x80-\x9F]|\xF0[\x90-\xBF][\x80-\xBF]?|[\xF1-\xF3][\x80-\xBF]{1,2}|\xF4[\x80-\x8F][\x80-\xBF]?|[\x80-\xFF])/', "\xFF", $s);
}
/**
* Folds the case of UTF-8 $s like the simple case folding of native mbstring.
*/
private static function foldCase(string $s): string
{
static $map = null;
if (null === $map) {
$map = array_combine(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1]) + self::getData('lowerCase');
unset($map["\xC4\xB0"]); // U+0130 has no simple case folding
}
return strtr($s, $map);
}
/**
* Counts the characters of UTF-8 $s like native mbstring, even when ill-formed: one per byte that is not a continuation byte.
*/
private static function utf8Length(string $s): int
{
return \strlen($s) - array_sum(\array_slice(count_chars($s), 0x80, 0x40));
}
/**
* @return int|false The offset in bytes of the character at $offset, false when it is out of range, like native mbstring
*/
private static function utf8Offset(string $s, int $offset, bool $bytes)
{
if (0 === $offset) {
return 0;
}
if ($bytes || preg_match('//u', $u = str_replace("\xFF", '?', $s))) {
$length = $bytes ? \strlen($s) : self::utf8Length($s);
if (0 > $offset) {
$offset += $length;
}
if (0 > $offset || $offset > $length) {
return false;
}
if ($bytes) {
return $offset;
}
for ($rx = ''; 65535 < $offset; $offset -= 65535) {
$rx .= '.{65535}';
}
preg_match('/^'.$rx.'.{'.$offset.'}\K/su', $u, $m, \PREG_OFFSET_CAPTURE);
return $m[0][1];
}
// Walk ill-formed UTF-8 like native mbstring: backward over the bytes that are not continuation bytes, forward by the lengths that lead bytes announce
$length = \strlen($s);
if (0 > $offset) {
$i = $length;
while (0 > $offset) {
if (0 === $i) {
return false;
}
if (0x80 !== (\ord($s[--$i]) & 0xC0)) {
++$offset;
}
}
return $i;
}
$i = 0;
while (0 < $offset--) {
if ($i >= $length) {
return false;
}
$c = \ord($s[$i]);
$i += $c < 0xC2 ? 1 : ($c < 0xE0 ? 2 : ($c < 0xF0 ? 3 : ($c < 0xF5 ? 4 : 1)));
}
return min($i, $length);
}
private static function html_encoding_callback(array $m)
{
$i = 1;
$entities = '';
$m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8'));
while (isset($m[$i])) {
if (0x80 > $m[$i]) {
$entities .= \chr($m[$i++]);
continue;
}
if (0xF0 <= $m[$i]) {
$c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
} elseif (0xE0 <= $m[$i]) {
$c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
} else {
$c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80;
}
$entities .= '&#'.$c.';';
}
return $entities;
}
private static function title_case(array $s)
{
static $titleCase = null;
if (null === $titleCase) {
$titleCase = self::getData('titleCase');
}
return ($titleCase[$s[1]] ?? self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8')).self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
}
private static function getData($file)
{
if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
return require $file;
}
return false;
}
private static function getEncoding($encoding)
{
if (null === $encoding) {
return self::$internalEncoding;
}
if ('UTF-8' === $encoding) {
return 'UTF-8';
}
$encoding = strtoupper($encoding);
if ('8BIT' === $encoding || 'BINARY' === $encoding) {
return 'CP850';
}
if ('UTF8' === $encoding) {
return 'UTF-8';
}
if ('UTF-32' === $encoding) {
return 'UTF-32BE';
}
if ('UTF-16' === $encoding) {
return 'UTF-16BE';
}
if ('UCS-2' === $encoding) {
return 'UCS-2BE';
}
return $encoding;
}
private static function iconv($fromEncoding, $toEncoding, $s)
{
if (null === self::$iconvSupportsIgnore) {
self::$iconvSupportsIgnore = false !== @iconv('UTF-8', 'UTF-8//IGNORE', '');
}
return self::$iconvSupportsIgnore
? iconv($fromEncoding, $toEncoding.'//IGNORE', $s)
: iconv($fromEncoding, $toEncoding, $s);
}
/** @return string|false */
private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function)
{
if (null === $encoding) {
$encoding = self::mb_internal_encoding();
} elseif (!self::assertEncoding($encoding, $function.'(): Argument #3 ($encoding) must be a valid encoding, "%s" given')) {
return false;
}
if ('' === $characters) {
return $string;
}
if ('UTF-8' === self::getEncoding($encoding)) {
$encoding = null;
if (!preg_match('//u', $string) || (null !== $characters && !preg_match('//u', $characters))) {
return self::mb_trim_invalid_utf8($string, $characters, $function);
}
} else {
$string = self::iconv($encoding, 'UTF-8', $string);
if (null !== $characters) {
$characters = self::iconv($encoding, 'UTF-8', $characters);
}
}
if (null === $characters) {
$characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}";
} else {
$characters = preg_quote($characters);
}
$string = preg_replace(\sprintf($regex, $characters), '', $string);
if (null === $encoding) {
return $string;
}
return self::iconv('UTF-8', $encoding, $string);
}
/**
* Trims ill-formed UTF-8 like mbstring: each maximal subpart of an ill-formed
* sequence counts as one character, and the string is returned as is when
* nothing is trimmed, else re-encoded with the substitute character.
*/
private static function mb_trim_invalid_utf8(string $string, ?string $characters, string $function): string
{
$regex = self::UTF8_CHAR_REGEX;
// Ill-formed sequences leave group 1 empty: they are trimmed when $characters has one too
preg_match_all($regex, $characters ?? "\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}", $m);
$trimmed = array_flip($m[1]);
$start = 0;
$end = \strlen($string);
while ('mb_rtrim' !== $function && $start < $end && preg_match($regex, $string, $m, 0, $start) && isset($trimmed[$m[1] ?? ''])) {
$start += \strlen($m[0]);
}
while ('mb_ltrim' !== $function && $start < $end) {
// Characters are at most 4 bytes long, and only continuation bytes don't start one
for ($i = $end - 1; $i > $start && $i > $end - 4 && 0x80 === (\ord($string[$i]) & 0xC0); --$i) {
}
preg_match_all($regex, substr($string, $i, $end - $i), $m);
if (!isset($trimmed[end($m[1])])) {
break;
}
$end -= \strlen(end($m[0]));
}
if (0 === $start && \strlen($string) === $end) {
return $string;
}
$string = substr($string, $start, $end - $start);
return 'none' === mb_substitute_character() ? self::scrubUtf8($string) : mb_convert_encoding($string, 'UTF-8', 'UTF-8');
}
private static function scrubUtf8(string $s): string
{
return preg_replace(self::UTF8_CHAR_REGEX, '$1', $s);
}
/**
* @param true[] $referenceIds The ids of the references followed to reach $array
*/
private static function convertArray(array $array, $toEncoding, $fromEncoding, array $referenceIds = []): array
{
$r = [];
foreach ($array as $key => $v) {
if (null !== $v && !\is_scalar($v) && !\is_array($v)) {
trigger_error('mb_convert_encoding(): Object is not supported', \E_USER_WARNING);
continue;
}
$k = \is_string($key) ? self::mb_convert_encoding($key, $toEncoding, $fromEncoding) : $key;
if (\is_string($v)) {
$v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding);
} elseif (\is_array($v)) {
// Only a reference can close a cycle: following the same one twice means one was found
$id = 70400 <= \PHP_VERSION_ID && ($reference = \ReflectionReference::fromArrayElement($array, $key)) ? $reference->getId() : null;
if (null !== $id && isset($referenceIds[$id])) {
trigger_error('mb_convert_encoding(): Cannot convert recursively referenced values', \E_USER_WARNING);
$v = [];
} else {
$v = self::convertArray($v, $toEncoding, $fromEncoding, null === $id ? $referenceIds : $referenceIds + [$id => true]);
}
}
// Keys that collide once converted keep their first value
$r += [$k => $v];
}
return $r;
}
/**
* Returns the error native mbstring throws for these encoding arguments, if any.
*/
private static function getConversionError(string $function, int $argument, $toEncoding, $fromEncoding): ?string
{
if (!self::isConvertibleEncoding((string) $toEncoding)) {
return \sprintf('%s(): Argument #%d ($to_encoding) must be a valid encoding, "%s" given', $function, $argument, $toEncoding);
}
if (null === $fromEncoding) {
return null;
}
if (!$list = \is_array($fromEncoding) ? $fromEncoding : ('' === $fromEncoding ? [] : explode(',', $fromEncoding))) {
return \sprintf('%s(): Argument #%d ($from_encoding) must specify at least one encoding', $function, 1 + $argument);
}
foreach ($list as $encoding) {
$encoding = \is_array($fromEncoding) ? (string) $encoding : trim($encoding, " \t");
// PHP 8.4 takes an empty entry of a comma-separated list for "auto"
if ('' === $encoding && !\is_array($fromEncoding) && 80400 <= \PHP_VERSION_ID) {
continue;
}
if (0 !== strcasecmp($encoding, 'auto') && !self::isConvertibleEncoding($encoding)) {
return \sprintf('%s(): Argument #%d ($from_encoding) contains invalid encoding "%s"', $function, 1 + $argument, $encoding);
}
}
return null;
}
private static function isConvertibleEncoding(string $encoding): bool
{
if (isset(self::$convertibleEncodings[$encoding])) {
return true;
}
$normalizedEncoding = self::getEncoding($encoding);
// Native encoding names use no other characters, while iconv() also accepts suffixes like //TRANSLIT
if ('' === $normalizedEncoding || preg_match('/[^\w#\-.:]/', $encoding) || (null === (self::UNSUPPORTED_CODEPOINT_ENCODINGS[$normalizedEncoding] ?? null) && false === @iconv($normalizedEncoding, $normalizedEncoding, ''))) {
return false;
}
return self::$convertibleEncodings[$encoding] = true;
}
private static function assertEncoding(string $encoding, string $errorFormat): bool
{
try {
$validEncoding = @self::mb_check_encoding('', $encoding);
} catch (\ValueError $e) {
throw new \ValueError(\sprintf($errorFormat, $encoding));
}
if (!$validEncoding) {
if (80000 > \PHP_VERSION_ID) {
trigger_error(\sprintf($errorFormat, $encoding), \E_USER_WARNING);
} else {
throw new \ValueError(\sprintf($errorFormat, $encoding));
}
}
return $validEncoding;
}
private static function assertCodepointEncoding(string $encoding, string $function): bool
{
if (null !== $name = self::UNSUPPORTED_CODEPOINT_ENCODINGS[strtoupper($encoding)] ?? null) {
if (80000 > \PHP_VERSION_ID) {
trigger_error(\sprintf('%s(): Unsupported encoding "%s"', $function, $name), \E_USER_WARNING);
return false;
}
throw new \ValueError(\sprintf('%s() does not support the "%s" encoding', $function, $name));
}
$normalizedEncoding = self::getEncoding($encoding);
if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, '')) {
return true;
}
if (80000 > \PHP_VERSION_ID) {
trigger_error(\sprintf('%s(): Unknown encoding "%s"', $function, $encoding), \E_USER_WARNING);
return false;
}
throw new \ValueError(\sprintf('%s(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $function, $encoding));
}
}