包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
639 lines
20 KiB
PHP
639 lines
20 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace MRBS\Mbstring;
|
|
|
|
use IntlChar;
|
|
use InvalidArgumentException;
|
|
use MRBS\Utf8\Utf8String;
|
|
use Normalizer;
|
|
use RuntimeException;
|
|
use Transliterator;
|
|
use ValueError;
|
|
|
|
/**
|
|
* A class that provides emulations of the PHP mbstring functions. This class should
|
|
* normally only be used by the emulation functions themselves or else test software.
|
|
* Only the UTF-8 encoding is supported.
|
|
*/
|
|
|
|
class Mbstring
|
|
{
|
|
/**
|
|
* A list of codepoints where the Transliterator provides a different result to
|
|
* the mbstring library when converting to lowercase.
|
|
*/
|
|
private const TRANSLITERATOR_LOWER_EXCEPTIONS = [
|
|
0x1C89 => 0x1C8A,
|
|
0xA7CB => 0x0264,
|
|
0xA7CC => 0xA7CD,
|
|
0xA7DA => 0xA7DB,
|
|
0xA7DC => 0x019B,
|
|
0x10D50 => 0x10D70,
|
|
0x10D51 => 0x10D71,
|
|
0x10D52 => 0x10D72,
|
|
0x10D53 => 0x10D73,
|
|
0x10D54 => 0x10D74,
|
|
0x10D55 => 0x10D75,
|
|
0x10D56 => 0x10D76,
|
|
0x10D57 => 0x10D77,
|
|
0x10D58 => 0x10D78,
|
|
0x10D59 => 0x10D79,
|
|
0x10D5A => 0x10D7A,
|
|
0x10D5B => 0x10D7B,
|
|
0x10D5C => 0x10D7C,
|
|
0x10D5D => 0x10D7D,
|
|
0x10D5E => 0x10D7E,
|
|
0x10D5F => 0x10D7F,
|
|
0x10D60 => 0x10D80,
|
|
0x10D61 => 0x10D81,
|
|
0x10D62 => 0x10D82,
|
|
0x10D63 => 0x10D83,
|
|
0x10D64 => 0x10D84,
|
|
0x10D65 => 0x10D85
|
|
];
|
|
|
|
/**
|
|
* A list of codepoints where the Transliterator provides a different result to
|
|
* the mbstring library when converting to uppercase.
|
|
*/
|
|
private const TRANSLITERATOR_UPPER_EXCEPTIONS = [
|
|
0x019B => 0xA7DC, // LATIN SMALL LETTER LAMBDA WITH STROKE
|
|
0x0264 => 0xA7CB, // LATIN SMALL LETTER RAMS HORN
|
|
0x1C8A => 0x1C89,
|
|
0xA7CD => 0xA7CC,
|
|
0xA7DB => 0xA7DA,
|
|
0x10D70 => 0x10D50,
|
|
0x10D71 => 0x10D51,
|
|
0x10D72 => 0x10D52,
|
|
0x10D73 => 0x10D53,
|
|
0x10D74 => 0x10D54,
|
|
0x10D75 => 0x10D55,
|
|
0x10D76 => 0x10D56,
|
|
0x10D77 => 0x10D57,
|
|
0x10D78 => 0x10D58,
|
|
0x10D79 => 0x10D59,
|
|
0x10D7A => 0x10D5A,
|
|
0x10D7B => 0x10D5B,
|
|
0x10D7C => 0x10D5C,
|
|
0x10D7D => 0x10D5D,
|
|
0x10D7E => 0x10D5E,
|
|
0x10D7F => 0x10D5F,
|
|
0x10D80 => 0x10D60,
|
|
0x10D81 => 0x10D61,
|
|
0x10D82 => 0x10D62,
|
|
0x10D83 => 0x10D63,
|
|
0x10D84 => 0x10D64,
|
|
0x10D85 => 0x10D65
|
|
];
|
|
|
|
|
|
/**
|
|
* Emulates mb_chr(). Only UTF-8 is supported.
|
|
*
|
|
* @return false|string
|
|
*/
|
|
public static function mb_chr(int $codepoint, ?string $encoding = null)
|
|
{
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
// Check if it's a codepoint reserved for surrogates.
|
|
if ((0xD800 <= $codepoint) && ($codepoint <= 0xDFFF))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Use IntlChar if possible - it's the fastest method.
|
|
if (method_exists('IntlChar', 'chr'))
|
|
{
|
|
return IntlChar::chr($codepoint);
|
|
}
|
|
|
|
// If not, use iconv(), if we can, which takes about 3 times as long as IntlChar.
|
|
if (function_exists('iconv'))
|
|
{
|
|
return iconv('UCS-4LE', 'UTF-8', pack('V', $codepoint));
|
|
}
|
|
|
|
// Otherwise, use json_decode() which takes about 5 times as long as IntlChar.
|
|
return json_decode('"' . self::FullJsonEscapedCodepoint($codepoint) . '"') ?? false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_ord(). Only UTF-8 is supported.
|
|
*
|
|
* @return false|int
|
|
*/
|
|
public static function mb_ord(string $string, ?string $encoding=null)
|
|
{
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
// Use IntlChar if possible - it's the fastest method.
|
|
if (method_exists('IntlChar', 'ord'))
|
|
{
|
|
return IntlChar::ord($string) ?? false;
|
|
}
|
|
|
|
// Taken from https://stackoverflow.com/questions/9361303/can-i-get-the-unicode-value-of-a-character-or-vise-versa-with-php
|
|
|
|
// If not, use iconv() if we can, which takes about 2.3 times as long as IntlChar.
|
|
if (function_exists('iconv'))
|
|
{
|
|
return unpack('V', iconv('UTF-8', 'UCS-4LE', $string))[1];
|
|
}
|
|
|
|
// Otherwise do it the long way, which takes about 2.6 times as long as IntlChar.
|
|
if (ord($string[0]) >= 0 && ord($string[0]) <= 127)
|
|
{
|
|
return ord($string[0]);
|
|
}
|
|
if (ord($string[0]) >= 192 && ord($string[0]) <= 223)
|
|
{
|
|
return (ord($string[0]) - 192) * 64 + (ord($string[1]) - 128);
|
|
}
|
|
if (ord($string[0]) >= 224 && ord($string[0]) <= 239)
|
|
{
|
|
return (ord($string[0]) - 224) * 4096 + (ord($string[1]) - 128) * 64 + (ord($string[2]) - 128);
|
|
}
|
|
if (ord($string[0]) >= 240 && ord($string[0]) <= 247)
|
|
{
|
|
return (ord($string[0]) - 240) * 262144 + (ord($string[1]) - 128) * 4096 + (ord($string[2]) - 128) * 64 + (ord($string[3]) - 128);
|
|
}
|
|
if (ord($string[0]) >= 248 && ord($string[0]) <= 251)
|
|
{
|
|
return (ord($string[0]) - 248) * 16777216 + (ord($string[1]) - 128) * 262144 + (ord($string[2]) - 128) * 4096 + (ord($string[3]) - 128) * 64 + (ord($string[4]) - 128);
|
|
}
|
|
if (ord($string[0]) >= 252 && ord($string[0]) <= 253)
|
|
{
|
|
return (ord($string[0]) - 252) * 1073741824 + (ord($string[1]) - 128) * 16777216 + (ord($string[2]) - 128) * 262144 + (ord($string[3]) - 128) * 4096 + (ord($string[4]) - 128) * 64 + (ord($string[5]) - 128);
|
|
}
|
|
if (ord($string[0]) >= 254 && ord($string[0]) <= 255) // error
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_regex_encoding(). Only UTF-8 is supported.
|
|
*
|
|
* @return string|true
|
|
*/
|
|
public static function mb_regex_encoding(?string $encoding=null)
|
|
{
|
|
if (!isset($encoding))
|
|
{
|
|
return 'UTF-8';
|
|
}
|
|
|
|
if (strtoupper($encoding) === 'UTF-8')
|
|
{
|
|
return true;
|
|
}
|
|
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_split(). Only UTF-8 is supported.
|
|
*
|
|
* @return array|false|string[]
|
|
*/
|
|
public static function mb_split(string $pattern, string $string, int $limit = -1)
|
|
{
|
|
return preg_split('/' . $pattern . '/u', $string, $limit);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_stripos(). Only UTF-8 is supported.
|
|
*
|
|
* @return false|int
|
|
* @throws ValueError if **offset** is greater than the length of the **haystack** (PHP 8.0 onwards)
|
|
*/
|
|
public static function mb_stripos(string $haystack, string $needle, int $offset=0, ?string $encoding=null)
|
|
{
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
// We could just convert $haystack and $needle to the same case and then use mb_strpos.
|
|
// However, that would involve converting the whole of both strings, whereas Utf8StrposGeneric()
|
|
// only converts those characters that are necessary.
|
|
return self::Utf8StrposGeneric($haystack, $needle, $offset, true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_strpos(). Only UTF-8 is supported.
|
|
*
|
|
* @return false|int
|
|
* @throws ValueError if **offset** is greater than the length of the **haystack** (PHP 8.0 onwards)
|
|
*/
|
|
public static function mb_strpos(string $haystack, string $needle, int $offset=0, ?string $encoding=null)
|
|
{
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
return self::Utf8StrposGeneric($haystack, $needle, $offset);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_strripos(). Only UTF-8 is supported.
|
|
*
|
|
* @return false|int
|
|
* @throws ValueError if **offset** is greater than the length of the **haystack** (PHP 8.0 onwards)
|
|
*/
|
|
public static function mb_strripos(string $haystack, string $needle, int $offset=0, ?string $encoding=null)
|
|
{
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
return self::Utf8StrposGeneric($haystack, $needle, $offset, true, true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_strrpos(). Only UTF-8 is supported.
|
|
*
|
|
* @return false|int
|
|
* @throws ValueError if **offset** is greater than the length of the **haystack** (PHP 8.0 onwards)
|
|
*/
|
|
public static function mb_strrpos(string $haystack, string $needle, int $offset=0, ?string $encoding=null)
|
|
{
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
return self::Utf8StrposGeneric($haystack, $needle, $offset, false, true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_strlen(). Only UTF-8, utf-8 and 8bit are supported.
|
|
*/
|
|
public static function mb_strlen(string $string, ?string $encoding=null) : int
|
|
{
|
|
if ($string === '')
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
switch ($encoding)
|
|
{
|
|
case null:
|
|
case 'UTF-8':
|
|
case 'utf-8':
|
|
$result = count((new Utf8String($string))->toArray());
|
|
break;
|
|
case '8bit':
|
|
$result = (new Utf8String($string))->byteCount();
|
|
break;
|
|
default:
|
|
throw new InvalidArgumentException("Encoding '$encoding' is not supported.");
|
|
break;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_strtolower(). Only UTF-8 is supported.
|
|
*/
|
|
public static function mb_strtolower(string $string, ?string $encoding=null) : string
|
|
{
|
|
if ($string === '')
|
|
{
|
|
return $string;
|
|
}
|
|
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
// If we can, use the Transliterator
|
|
if (method_exists('Transliterator', 'transliterate'))
|
|
{
|
|
// Works better than IntlChar::toLower()
|
|
// See https://stackoverflow.com/questions/79655507/what-is-the-difference-between-phps-mb-strolower-and-intlchartolower
|
|
return self::TransliteratorToLower($string);
|
|
}
|
|
|
|
// Otherwise, use the (enhanced) ordinary strtolower().
|
|
return Ordinary::strtolower($string);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_strtoupper(). Only UTF-8 is supported.
|
|
*/
|
|
public static function mb_strtoupper(string $string, ?string $encoding=null) : string
|
|
{
|
|
if ($string === '')
|
|
{
|
|
return $string;
|
|
}
|
|
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
// If we can, use the Transliterator
|
|
if (method_exists('Transliterator', 'transliterate'))
|
|
{
|
|
// Works better than IntlChar::toUpper()
|
|
// See https://stackoverflow.com/questions/79655507/what-is-the-difference-between-phps-mb-strolower-and-intlchartolower
|
|
return self::TransliteratorToUpper($string);
|
|
}
|
|
|
|
// Otherwise, use the (enhanced) ordinary strtoupper().
|
|
return Ordinary::strtoupper($string);
|
|
}
|
|
|
|
|
|
/**
|
|
* Emulates mb_substr(). Only UTF-8 is supported.
|
|
*/
|
|
public static function mb_substr(string $string, int $start, ?int $length = null, ?string $encoding = null): string
|
|
{
|
|
if (isset($encoding) && (strtoupper($encoding) !== 'UTF-8'))
|
|
{
|
|
$message = "This emulation of " . __FUNCTION__ . "() only supports the UTF-8 encoding.";
|
|
throw new InvalidArgumentException($message);
|
|
}
|
|
|
|
return implode('', array_slice((new Utf8String($string))->toArray(), $start, $length));
|
|
}
|
|
|
|
|
|
/**
|
|
* A generic emulation of mb_strpos(), mb_stripos(), mb_strrpos() and mb_strripos() for UTF-8
|
|
* that has the case-sensitivity and direction as parameters.
|
|
* This function can be tested by test_mb.php in the mrbs-tools repository.
|
|
*
|
|
* @return false|int
|
|
* @throws ValueError if **offset** is greater than the length of the **haystack** (PHP 8.0 onwards)
|
|
*/
|
|
private static function Utf8StrposGeneric(string $haystack, string $needle, int $offset=0, bool $case_insensitive=false, bool $reverse=false)
|
|
{
|
|
$haystack_chars = (new Utf8String($haystack))->toArray();
|
|
$last_haystack_index = count($haystack_chars) - 1;
|
|
$needle_chars = (new Utf8String($needle))->toArray();
|
|
$last_needle_index = count($needle_chars) - 1;
|
|
|
|
if ($reverse)
|
|
{
|
|
// We're looking for the last occurrence of the needle in the haystack, so we
|
|
// start at the right-hand end of the haystack, going backwards through the
|
|
// haystack looking for the right-hand end of the needle. If we find the end
|
|
// of the needle, we keep going backwards through the needle and haystack,
|
|
// checking that all the characters match.
|
|
$increment = -1;
|
|
if ($offset < 0)
|
|
{
|
|
// The PHP mb_strrpos() documentation isn't very clear. If there's a negative offset
|
|
// then it looks for a needle that can *start* anywhere up to and including the offset
|
|
// character. It doesn't matter if part of the needle is to the right of the offset.
|
|
$haystack_start = min($last_haystack_index, $last_haystack_index + count($needle_chars) + $offset);
|
|
$haystack_end = 0;
|
|
}
|
|
else
|
|
{
|
|
$haystack_start = $last_haystack_index;
|
|
$haystack_end = $offset;
|
|
}
|
|
$needle_start = $last_needle_index;
|
|
$needle_end = 0;
|
|
}
|
|
else
|
|
{
|
|
// We're looking for the first occurrence of the needle in the haystack, so we
|
|
// start at the left-hand end of the haystack, going forwards through the
|
|
// haystack looking for the left-hand end of the needle. If we find the end
|
|
// of the needle, we keep going forwards through the needle and haystack,
|
|
// checking that all the characters match.
|
|
$increment = 1;
|
|
// Note the +1 for negative offsets n the line below: it's the way the PHP implementation works
|
|
$haystack_start = ($offset < 0) ? $last_haystack_index + $offset + 1 : $offset;
|
|
$haystack_end = $last_haystack_index;
|
|
$needle_start = 0;
|
|
$needle_end = $last_needle_index;
|
|
}
|
|
|
|
// Check that $offset is sensible. (The PHP implementation allows an offset of +/-N on a
|
|
// string of length N. The error reporting in the PHP implementation depends on the PHP version.)
|
|
if (abs($offset) > $last_haystack_index + 1)
|
|
{
|
|
$message = __FUNCTION__ . '(): Argument #3 ($offset) must be contained in argument #1 ($haystack)';
|
|
|
|
if ((version_compare(PHP_VERSION, '8') >= 0))
|
|
{
|
|
throw new ValueError($message);
|
|
}
|
|
else
|
|
{
|
|
$message = __FUNCTION__ . '(): Offset not contained in string';
|
|
trigger_error($message, E_USER_WARNING);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Special case: an empty needle is found immediately
|
|
if ($needle === '')
|
|
{
|
|
return $haystack_start;
|
|
}
|
|
|
|
// Quick test: if the needle is longer than the part of the haystack to
|
|
// be searched, then we'll never find it.
|
|
if ($reverse)
|
|
{
|
|
if ($offset >= 0)
|
|
{
|
|
if ((count($needle_chars) > $haystack_start + 2 - $haystack_end))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
elseif (count($needle_chars) > count($haystack_chars))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
elseif (count($needle_chars) > $haystack_end + 1 - $haystack_start)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Otherwise, start searching through the haystack for the needle (going either backwards
|
|
// or forwards depending on the value of $increment).
|
|
$n = $needle_start;
|
|
|
|
for ($h = $haystack_start; ($increment > 0) ? $h <= $haystack_end : $h >= $haystack_end; $h += $increment)
|
|
{
|
|
// If we get a match with a needle character, then keep cycling through
|
|
// the haystack and needle. If we get to the end of the needle and all
|
|
// characters have matched, then we've found the needle; otherwise we
|
|
// reset and start looking for the needle again.
|
|
while ((($increment > 0) ? $h <= $haystack_end : $h >= $haystack_end) && isset($needle_chars[$n]) &&
|
|
self::isStringEqual($haystack_chars[$h], $needle_chars[$n], $case_insensitive))
|
|
{
|
|
if ($n === $needle_end)
|
|
{
|
|
// We've found the needle, as we've got to the end and all characters match
|
|
return $h - $n; // to get the starting index of the needle.
|
|
}
|
|
$h += $increment;
|
|
$n += $increment;
|
|
}
|
|
// No match, reset the needle index
|
|
$n = $needle_start;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
private static function TransliteratorToLower(string $string) : string
|
|
{
|
|
// There are some characters that the Transliterator treats differently from the mbstring
|
|
// library, so we have to split the string into characters and deal with them one by one.
|
|
// Remember, we are only trying to emulate the mbstring library, not provide the "correct"
|
|
// result.
|
|
$result = '';
|
|
$utf8_string = new Utf8String($string);
|
|
|
|
foreach ($utf8_string->toArray() as $char)
|
|
{
|
|
$codepoint = IntlChar::ord($char);
|
|
if (array_key_exists($codepoint, self::TRANSLITERATOR_LOWER_EXCEPTIONS))
|
|
{
|
|
$result .= IntlChar::chr(self::TRANSLITERATOR_LOWER_EXCEPTIONS[$codepoint]);
|
|
}
|
|
else
|
|
{
|
|
$result .= Transliterator::create('Lower')->transliterate($char);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
private static function TransliteratorToUpper(string $string) : string
|
|
{
|
|
// There are some characters that the Transliterator treats differently from the mbstring
|
|
// library, so we have to split the string into characters and deal with them one by one.
|
|
// Remember, we are only trying to emulate the mbstring library, not provide the "correct"
|
|
// result.
|
|
$result = '';
|
|
$utf8_string = new Utf8String($string);
|
|
|
|
foreach ($utf8_string->toArray() as $char)
|
|
{
|
|
$codepoint = IntlChar::ord($char);
|
|
if (array_key_exists($codepoint, self::TRANSLITERATOR_UPPER_EXCEPTIONS))
|
|
{
|
|
$result .= IntlChar::chr(self::TRANSLITERATOR_UPPER_EXCEPTIONS[$codepoint]);
|
|
}
|
|
else
|
|
{
|
|
$result .= Transliterator::create('Upper')->transliterate($char);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
private static function SingleJsonEscapedCodepoint(int $codepoint) : string
|
|
{
|
|
return '\u' . str_pad(strtoupper(dechex($codepoint)), 4, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
|
|
private static function FullJsonEscapedCodepoint(int $codepoint) : string
|
|
{
|
|
if ($codepoint > 0x10FFFF)
|
|
{
|
|
throw new InvalidArgumentException('Codepoint cannot be greater than 0x10FFFF');
|
|
}
|
|
|
|
// The simple case: codepoints in the Basic Multilingual Plane
|
|
if ($codepoint < 0x10000)
|
|
{
|
|
return self::SingleJsonEscapedCodepoint($codepoint);
|
|
}
|
|
|
|
// Otherwise, we have to split the codepoint into surrogate pairs. See https://en.wikipedia.org/wiki/UTF-16
|
|
// Subtract 0x10000, leaving a 20-bit number
|
|
$codepoint -= 0x10000;
|
|
|
|
// For the leading surrogate, take the high 10 bits and add 0xd800
|
|
$leading = self::SingleJsonEscapedCodepoint(($codepoint >> 10) + 0xD800);
|
|
|
|
// For the trailing surrogate, take the low 10 bits and add 0xdc00
|
|
$trailing = self::SingleJsonEscapedCodepoint(($codepoint & 0x3FF) + 0xDC00);
|
|
|
|
return $leading . $trailing;
|
|
}
|
|
|
|
|
|
/**
|
|
* Test to see if two strings are equal, optionally case-insensitively.
|
|
* See https://stackoverflow.com/questions/5473542/case-insensitive-string-comparison
|
|
*/
|
|
private static function isStringEqual(string $string1, string $string2, bool $case_insensitive=false) : bool
|
|
{
|
|
// Case-sensitive test
|
|
if (!$case_insensitive)
|
|
{
|
|
return ($string1 === $string2);
|
|
}
|
|
|
|
// Case-insensitive
|
|
// Quick test to see if they are equal in a case-sensitive fashion.
|
|
if ($string1 === $string2)
|
|
{
|
|
return true;
|
|
}
|
|
// Otherwise do a case-insensitive check
|
|
if (method_exists('Normalizer', 'normalize'))
|
|
{
|
|
$string1 = Normalizer::normalize($string1, Normalizer::FORM_KC);
|
|
$string2 = Normalizer::normalize($string2, Normalizer::FORM_KC);
|
|
}
|
|
return ((mb_strtolower($string1) === mb_strtolower($string2)) ||
|
|
(mb_strtoupper($string1) === mb_strtoupper($string2)));
|
|
}
|
|
|
|
}
|