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
+417
View File
@@ -0,0 +1,417 @@
<?php
declare(strict_types=1);
namespace MRBS\Intl;
// A class provides a basic emulation of PHP's IntlDateFormatter class.
//
// The emulation uses the deprecated function strftime() and is only necessary for older
// PHP systems where the Intl extension isn't available. Eventually the emulation can be
// dispensed with.
//
// Note that some servers have out of date versions of the ICU library that can't be updated
// easily. In those cases better results can sometimes be achieved by using strftime() and
// this can be forced by explicitly using this class.
use DateTimeInterface;
use MRBS\Exception;
use MRBS\Language;
use MRBS\System;
// We need to check that the 'intl' extension is loaded because earlier versions of
// MRBS had the IntlDateFormatter emulation class at the top level in lib. If users
// have upgraded by just overwriting files without deleting that file, then it will
// be picked up by the class_exists() test and used instead of the more up-to-date
// emulation below.
// Note that there is a polyfill for IntlDateFormatter available at
// https://github.com/symfony/polyfill-intl-icu, but it is limited to the 'en' locale.
// There are also backwards compatibility versions of strftime() available, but
// IntlDateFormatter is a more powerful solution.
class IntlDateFormatter
{
const FULL = 0;
const LONG = 1;
const MEDIUM = 2;
const SHORT = 3;
const NONE = -1;
const RELATIVE_FULL = 128; // Available as of PHP 8.0.0, for dateType only
const RELATIVE_LONG = 129; // Available as of PHP 8.0.0, for dateType only
const RELATIVE_MEDIUM = 130; // Available as of PHP 8.0.0, for dateType only
const RELATIVE_SHORT = 131; // Available as of PHP 8.0.0, for dateType only
const GREGORIAN = 1;
const TRADITIONAL = 0;
private const TYPE_NAMES = array(
self::FULL => 'full',
self::LONG => 'long',
self::MEDIUM => 'medium',
self::SHORT => 'short',
self::NONE => 'none'
);
private const DEFAULT_LOCALE = 'en';
private $locale;
private $dateType;
private $timeType;
private $timezone;
private $calendar;
private $pattern;
public function __construct(
?string $locale,
int $dateType = self::FULL,
int $timeType = self::FULL,
$timezone = null,
$calendar = null,
?string $pattern = null)
{
if (!function_exists('strftime'))
{
throw new Exception("Neither the IntlDateFormatter class nor the strftime() function exist on this server");
}
// Emulate PHP 8.4 and later by detecting invalid locales now, in order to avoid problems later on.
if (isset($locale) && !System::isAvailableLocale($locale))
{
$message = 'Argument #1 ($locale) "' . $locale . '" is invalid';
$throwable = (version_compare(PHP_VERSION, '8.0') >= 0) ? '\ValueError' : '\Exception';
throw new $throwable($message);
}
$this->locale = $locale;
$this->dateType = $dateType;
$this->timeType = $timeType;
$this->timezone = $timezone ?? date_default_timezone_get();
$this->calendar = $calendar ?? self::GREGORIAN;
if (!isset($pattern)) {
$file = MRBS_ROOT . "/intl/types/" .
self::TYPE_NAMES[$this->dateType] . "_" . self::TYPE_NAMES[$this->timeType] . ".ini";
if (is_readable($file)) {
$patterns = parse_ini_file($file);
if (!empty($patterns)) {
$pattern = $patterns[Language::convertToBcp47($this->locale)] ?? $patterns[self::DEFAULT_LOCALE] ?? null;
}
}
}
if (!isset($pattern)) {
throw new Exception("Could not get pattern");
}
$this->setPattern($pattern);
}
public function format($datetime)
{
// $datetime can be many types
// TODO: Handle the remaining possible types
if ($datetime instanceof DateTimeInterface)
{
$timestamp = $datetime->getTimestamp();
}
else
{
$timestamp = (int)$datetime;
}
$converter = new IntlDatePatternConverter(new FormatterStrftime());
return $this->strftimePlus($converter->convert($this->pattern), $timestamp);
}
//Get the calendar type used for the IntlDateFormatter
public function getCalendar()
{
return $this->calendar ?? false;
}
// Get the datetype used for the IntlDateFormatter
public function getDateType()
{
return $this->dateType ?? false;
}
// Get the locale used by formatter
public function getLocale(int $type=Locale::ACTUAL_LOCALE)
{
switch ($type)
{
// TODO: Do something with $type, though it's not exactly clear what the difference is
// TODO: between the two types. See also https://www.php.net/manual/en/collator.getlocale.php
case LOCALE::ACTUAL_LOCALE:
case LOCALE::VALID_LOCALE:
return $this->locale ?? false;
break;
default:
return false;
break;
}
}
// Get the pattern used for the IntlDateFormatter
public function getPattern()
{
return $this->pattern ?? false;
}
// Get the timetype used for the IntlDateFormatter
public function getTimeType()
{
return $this->timeType ?? false;
}
// The standard PHP version can also return false
public function setPattern(string $pattern): bool
{
$this->pattern = $pattern;
return true;
}
// Format a local time/date according to locale settings, returning the
// result as a UTF-8 string. This function is based on strftime()
// $time can be an int or a float (union type declarations not supported until PHP 8.0)
// $locale can either be a string or an array of locales. If $locale
// is not set then the current locale is used.
//
// This method extends the standard PHP strftime() function and adds extra formats:
//
// %f Numeric representation of the month 1 (for January) through 12 (for December)
// without leading zeroes. Won't
// necessarily work in locales that don't
// use [0..9] for the month.
//
// %i One/two digit day of the month, with no 1 to 31
// leading space
//
// %o Hour in 12-hour format, with no space 1 through 12
// preceding single digits
//
// %q Minute in the hour, with no leading zero 4
//
// %v Seconds, with no leading zero
//
// %E Day of year, with no leading zeroes
private function strftimePlus(string $format, int $timestamp): string
{
$server_os = System::getServerOS();
// Set the temporary locale. Note that $this->locale could be an array of locales,
// so we need to find out which locale actually worked.
if (!empty($this->locale)) {
$old_locale = setlocale(LC_TIME, '0');
if (false === ($new_locale = Language::setLocale(LC_TIME, $this->locale)))
{
$new_locale = $old_locale;
$locale = is_array($this->locale) ? json_encode($this->locale) : "'$this->locale'";
$message = "Could not set locale to $locale; continuing to use '$old_locale'";
trigger_error($message, E_USER_WARNING);
}
}
elseif ($server_os == "windows") {
// If we are running Windows we have to set the locale again in case another script
// running in the same process has changed the locale since we first set it. See the
// warning on the PHP manual page for setlocale():
//
// "The locale information is maintained per process, not per thread. If you are
// running PHP on a multithreaded server API like IIS or Apache on Windows, you may
// experience sudden changes in locale settings while a script is running, though
// the script itself never called setlocale(). This happens due to other scripts
// running in different threads of the same process at the same time, changing the
// process-wide locale using setlocale()."
$new_locale = Language::getInstance()->getWebLocale();
Language::setLocale(LC_ALL, $new_locale);
}
else {
$new_locale = null;
}
$result = self::doStrftimePlus($format, $timestamp, $new_locale);
// Restore the original locale
if (!empty($this->locale)) {
setlocale(LC_TIME, $old_locale);
}
return $result;
}
/**
* Wrapper for strftime() that suppresses deprecation errors
*
* @return false|string
*/
private static function doStrftime(string $format, ?int $timestamp = null)
{
assert(version_compare(MRBS_MIN_PHP_VERSION, '8.0.0', '<'), "The line below can be removed.");
$timestamp = $timestamp ?? time(); // $timestamp only became nullable in strftime() in PHP 8.0.0
// Temporarily suppress deprecation errors so that we are not flooded with them.
// We have a single message in init.inc.
$error_level = error_reporting();
error_reporting($error_level & ~E_DEPRECATED);
$result = strftime($format, $timestamp);
error_reporting($error_level);
return $result;
}
/**
* Test whether a specifier is supported by strftime() and replace it with an alternative if not.
*/
private static function testAndReplaceFormat(string $specifier, string $replacement, string $format) : string
{
if (false === self::doStrftime($specifier))
{
return str_replace($specifier, $replacement, $format);
}
return $format;
}
private static function doStrftimePlus(string $format, int $timestamp, ?string $locale): string
{
// Test whether certain specifiers are supported on this OS for this locale. We do an actual test,
// rather than just checking which OS we are running on, because it is more reliable.
$doubtful_specifiers = [
'%R' => '%H:%M', // Not supported on Windows
'%P' => '%p', // Not supported on Windows, macOS and also some locales
'%l' => '%I', // Not supported on Windows
'%e' => '%#d' // Not supported on Windows
];
foreach ($doubtful_specifiers as $specifier => $replacement)
{
$format = self::testAndReplaceFormat($specifier, $replacement, $format);
}
// %p doesn't actually work in some locales, so we have to patch it up ourselves by using
// date() instead of strftime().
// Note that we may be using %p instead of %P, because %P isn't supported for this locale. If that's
// the case we're going to get a lowercase result, instead of uppercase as intended. But that
// probably doesn't matter as that locale would almost certainly be using a 24-hour format anyway,
// which is why %P isn't supported in the first place.
if (preg_match('/%p/', $format) && (false === self::doStrftime('%p', $timestamp)))
{
$format = preg_replace('/%p/', date('a', $timestamp), $format);
}
$result = '';
// Split the format into individual tokens so that we can process our extensions
$tokens = self::parseStrftimeFormat($format);
foreach ($tokens as $token) {
if (mb_strlen($token) === 1) {
$result .= $token;
}
else {
switch ($token) {
case '%E':
// We want the day of the year without leading zeroes.
$formatted = self::doStrftimePlus('%j', $timestamp, $locale);
$formatted = ltrim($formatted, '0');
break;
case '%f':
// We want a month number without leading zeroes. We can't use date('n', $time)
// because date will return an English answer with a month made up of the characters
// [0..9] which won't be correct for all locales.
$formatted = self::doStrftimePlus('%m', $timestamp, $locale);
$formatted = ($formatted === '00') ? '0' : ltrim($formatted, '0');
break;
case '%i':
$formatted = ltrim(self::doStrftimePlus('%e', $timestamp, $locale));
break;
case '%J':
// We want the week of the year without leading zeroes.
$formatted = self::doStrftimePlus('%V', $timestamp, $locale);
$formatted = ltrim($formatted, '0');
break;
case '%o':
$formatted = ltrim(self::doStrftimePlus('%l', $timestamp, $locale));
break;
case '%q':
// We want a minute without leading zeroes.
$formatted = self::doStrftimePlus('%M', $timestamp, $locale);
$formatted = ($formatted === '00') ? '0' : ltrim($formatted, '0');
break;
case '%v':
// We want seconds without leading zeroes.
$formatted = self::doStrftimePlus('%S', $timestamp, $locale);
$formatted = ($formatted === '00') ? '0' : ltrim($formatted, '0');
break;
default:
$formatted = self::doStrftime($token, $timestamp);
break;
}
$result .= System::utf8ConvertFromLocale($formatted, $locale);
}
}
return $result;
}
// Parses a strftime format into an array of strings, which will either be two or three-character
// formats or one-character text strings.
private static function parseStrftimeFormat(string $format): array
{
$result = array();
// Split the format into an array of multibyte characters
$chars = preg_split("//u", $format, 0, PREG_SPLIT_NO_EMPTY);
while (null !== ($char = array_shift($chars))) {
if ($char !== '%') {
// It's ordinary text
$result[] = $char;
}
else {
// Get the next character which will either be a conversion specifier or an escaped character
$char = array_shift($chars);
switch ($char) {
case null:
throw new Exception("Invalid format '$format'");
break;
case 'n':
$result[] = "\n";
break;
case 't':
$result[] = "\t";
break;
case '%':
$result[] = "%";
break;
case '#':
// This covers the case of '%#d' on Windows
$char = array_shift($chars);
if (!isset($char)) {
throw new Exception("Invalid format '$format'");
}
else {
$result [] = "%#$char";
}
break;
default:
$result [] = "%$char";
break;
}
}
}
return $result;
}
}