mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-10-29 12:43:25 +00:00
1663 lines
59 KiB
PHP
1663 lines
59 KiB
PHP
<?php
|
||
|
||
/*
|
||
* This file is part of the Symfony package.
|
||
*
|
||
* (c) Fabien Potencier <fabien@symfony.com>
|
||
*
|
||
* For the full copyright and license information, please view the LICENSE
|
||
* file that was distributed with this source code.
|
||
*/
|
||
|
||
namespace Symfony\Polyfill\Mbstring;
|
||
|
||
/**
|
||
* 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
|
||
*
|
||
* @author Nicolas Grekas <p@tchwork.com>
|
||
*
|
||
* @internal
|
||
*/
|
||
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));
|
||
}
|
||
}
|