MRBS 1.12.2 等保2.0二级整改完整提交

包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、JS空集合保护、
会话过期体验优化(403 JSON)、display_errors 关闭、
固定 key 根治 Integrity check failed 等全部改动

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
This commit is contained in:
人事系统开发
2026-09-09 16:55:02 +08:00
commit 1ba6efd8ed
2151 changed files with 528780 additions and 0 deletions
+638
View File
@@ -0,0 +1,638 @@
<?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)));
}
}
File diff suppressed because it is too large Load Diff