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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user