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,311 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A partial, very basic, emulation of the PHP \Collator class. Some methods are not implemented, and
|
||||
* some attributes are not supported. This class is only intended as a very basic fallback if the intl
|
||||
* extension is not available. For best results use the intl extension.
|
||||
* @see \Collator
|
||||
*/
|
||||
class Collator
|
||||
{
|
||||
public const DEFAULT_VALUE = -1;
|
||||
public const PRIMARY = 0;
|
||||
public const SECONDARY = 1;
|
||||
public const TERTIARY = 2;
|
||||
public const DEFAULT_STRENGTH = 2;
|
||||
public const QUATERNARY = 3;
|
||||
public const IDENTICAL = 15;
|
||||
public const OFF = 16;
|
||||
public const ON = 17;
|
||||
public const SHIFTED = 20;
|
||||
public const NON_IGNORABLE = 21;
|
||||
public const LOWER_FIRST = 24;
|
||||
public const UPPER_FIRST = 25;
|
||||
public const FRENCH_COLLATION = 0;
|
||||
public const ALTERNATE_HANDLING = 1;
|
||||
public const CASE_FIRST = 2;
|
||||
public const CASE_LEVEL = 3;
|
||||
public const NORMALIZATION_MODE = 4;
|
||||
public const STRENGTH = 5;
|
||||
public const HIRAGANA_QUATERNARY_MODE = 6;
|
||||
public const NUMERIC_COLLATION = 7;
|
||||
public const SORT_REGULAR = 0;
|
||||
public const SORT_STRING = 1;
|
||||
public const SORT_NUMERIC = 2;
|
||||
|
||||
/**
|
||||
* Default values for attributes.
|
||||
* @see \Collator
|
||||
*/
|
||||
private const ATTRIBUTES_DEFAULT_VALUES = [
|
||||
self::FRENCH_COLLATION => self::OFF,
|
||||
self::ALTERNATE_HANDLING => self::NON_IGNORABLE,
|
||||
self::CASE_FIRST => self::OFF,
|
||||
self::CASE_LEVEL => self::OFF,
|
||||
self::NORMALIZATION_MODE => self::OFF,
|
||||
self::STRENGTH => self::DEFAULT_STRENGTH,
|
||||
self::HIRAGANA_QUATERNARY_MODE => self::OFF,
|
||||
self::NUMERIC_COLLATION => self::OFF
|
||||
];
|
||||
|
||||
/**
|
||||
* Possible values for attributes.
|
||||
* @see \Collator
|
||||
*/
|
||||
private const ATTRIBUTES_POSSIBLE_VALUES = [
|
||||
self::FRENCH_COLLATION => [self::ON, self::OFF, self::DEFAULT_VALUE],
|
||||
self::ALTERNATE_HANDLING => [self::NON_IGNORABLE, self::SHIFTED, self::DEFAULT_VALUE],
|
||||
self::CASE_FIRST => [self::OFF, self::LOWER_FIRST, self::UPPER_FIRST, self::DEFAULT_VALUE],
|
||||
self::CASE_LEVEL => [self::OFF, self::ON, self::DEFAULT_VALUE],
|
||||
self::NORMALIZATION_MODE => [self::OFF, self::ON, self::DEFAULT_VALUE],
|
||||
self::STRENGTH => [self::PRIMARY, self::SECONDARY, self::TERTIARY, self::QUATERNARY, self::IDENTICAL, self::DEFAULT_STRENGTH],
|
||||
self::HIRAGANA_QUATERNARY_MODE => [self::OFF, self::ON, self::DEFAULT_VALUE],
|
||||
self::NUMERIC_COLLATION => [self::OFF, self::ON, self::DEFAULT_VALUE],
|
||||
];
|
||||
|
||||
private $attributes = [];
|
||||
private $locale;
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::__construct()
|
||||
*/
|
||||
public function __construct(string $locale)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
// Set the default values for the attributes
|
||||
foreach(self::ATTRIBUTES_DEFAULT_VALUES as $attribute => $default_value)
|
||||
{
|
||||
$this->setAttribute($attribute, $default_value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::asort()
|
||||
*/
|
||||
public function asort(array &$array, int $flags = self::SORT_REGULAR): bool
|
||||
{
|
||||
return $this->genericSort(true, $array, $flags);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::sort()
|
||||
*/
|
||||
public function sort(array &$array, int $flags = self::SORT_REGULAR): bool
|
||||
{
|
||||
return $this->genericSort(false, $array, $flags);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int<-1,1>|false
|
||||
* @see \Collator::compare()
|
||||
*/
|
||||
public function compare(string $string1, string $string2)
|
||||
{
|
||||
// Primary and secondary strengths are case-insensitive. The sort() method in this class
|
||||
// cannot perform a locale aware, case-insensitive sort, so make the two strings the same
|
||||
// case here, before trying the sort.
|
||||
if (in_array($this->getStrength(), [self::PRIMARY, self::SECONDARY], true))
|
||||
{
|
||||
$string1 = mb_strtolower($string1);
|
||||
$string2 = mb_strtolower($string2);
|
||||
}
|
||||
|
||||
// Trivial case
|
||||
if ($string1 === $string2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sort the array. If the order is reversed, then $string1 > $string2.
|
||||
// (When this function is being used as a callback for usort, and if the original array
|
||||
// is sorted in ascending order - which it well might be if it's the result of
|
||||
// an SQL query with an ORDER BY - then it's fastest to test for $string1 > $string2
|
||||
// first, as below.)
|
||||
$original_array = [$string1, $string2];
|
||||
$array = $original_array;
|
||||
$this->sort($array);
|
||||
if ($array !== $original_array)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Otherwise, flip the array and try again. If the order is reversed, then $string2 > $string1.
|
||||
$original_array = [$string2, $string1];
|
||||
$array = $original_array;
|
||||
$this->sort($array);
|
||||
if ($array !== $original_array)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Otherwise they must be equal
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::create()
|
||||
*/
|
||||
public static function create(string $locale): ?self
|
||||
{
|
||||
return new self($locale);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int|false
|
||||
* @see \Collator::getAttribute()
|
||||
*/
|
||||
public function getAttribute(int $attribute)
|
||||
{
|
||||
if (!array_key_exists($attribute, $this->attributes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->attributes[$attribute];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return int|false
|
||||
* @see \Collator::getErrorCode()
|
||||
*/
|
||||
public function getErrorCode()
|
||||
{
|
||||
throw new Exception("Not yet implemented");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|false
|
||||
* @see \Collator::getErrorMessage()
|
||||
*/
|
||||
public function getErrorMessage()
|
||||
{
|
||||
throw new Exception("Not yet implemented");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|false
|
||||
* @see \Collator::getLocale()
|
||||
*/
|
||||
public function getLocale()
|
||||
{
|
||||
throw new Exception("Not yet implemented");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string|false
|
||||
* @see \Collator::getSortKey()
|
||||
*/
|
||||
public function getSortKey(string $string)
|
||||
{
|
||||
throw new Exception("Not yet implemented");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::getStrength()
|
||||
*/
|
||||
public function getStrength(): int
|
||||
{
|
||||
return $this->getAttribute(self::STRENGTH);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::setAttribute()
|
||||
*/
|
||||
public function setAttribute(int $attribute, int $value): bool
|
||||
{
|
||||
if (!in_array($value, self::ATTRIBUTES_POSSIBLE_VALUES[$attribute]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: The manual (https://www.php.net/manual/en/class.collator.php#collator.constants.french-collation)
|
||||
// TODO: says that FRENCH_COLLATION "is automatically set to On for the French locales and a few others".
|
||||
// TODO: However, this doesn't seem to be the case in testing: it's always Off. Probably doesn't matter
|
||||
// TODO: in practice though as this emulator won't be able to do anything about it anyway.
|
||||
$this->attributes[$attribute] = ($value === self::DEFAULT_VALUE) ? self::ATTRIBUTES_DEFAULT_VALUES[$attribute] : $value;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::setStrength()
|
||||
* @return true
|
||||
*/
|
||||
public function setStrength(int $strength)
|
||||
{
|
||||
$this->setAttribute(self::STRENGTH, $strength);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \Collator::sortWithSortKeys()
|
||||
*/
|
||||
public function sortWithSortKeys(array &$array): bool
|
||||
{
|
||||
throw new Exception("Not yet implemented");
|
||||
}
|
||||
|
||||
|
||||
private function genericSort(bool $maintain_index_association, array &$array, int $flags = self::SORT_REGULAR): bool
|
||||
{
|
||||
$locale_switcher = new LocaleSwitcher(LC_COLLATE, $this->locale);
|
||||
$locale_switcher->switch();
|
||||
|
||||
// Convert the flags to the equivalent value for the ordinary functions sort()/asort().
|
||||
// Note that the sort()/asort() flags can be just one of SORT_REGULAR, SORT_NUMERIC, SORT_STRING, SORT_LOCALE_STRING or SORT_NATURAL.
|
||||
// Then SORT_FLAG_CASE can be combined with SORT_STRING or SORT_NATURAL.
|
||||
// This means that the ordinary PHP sort() functions do not support locale-aware natural or case-insensitive sorting.
|
||||
switch ($flags)
|
||||
{
|
||||
case self::SORT_STRING:
|
||||
case self::SORT_REGULAR:
|
||||
// If NUMERIC_COLLATION is on, then use SORT_NATURAL, otherwise use SORT_LOCALE_STRING
|
||||
$ordinary_flags = ($this->getAttribute(self::NUMERIC_COLLATION) === self::ON) ? SORT_NATURAL : SORT_LOCALE_STRING;
|
||||
break;
|
||||
case self::SORT_NUMERIC:
|
||||
$ordinary_flags = SORT_NUMERIC;
|
||||
break;
|
||||
default:
|
||||
throw new \InvalidArgumentException("Invalid flags value '$flags'");
|
||||
break;
|
||||
}
|
||||
|
||||
// Primary and secondary strengths are case-insensitive.
|
||||
// SORT_FLAG_CASE can only be used with SORT_STRING or SORT_NATURAL.
|
||||
if (in_array($ordinary_flags, [SORT_STRING, SORT_NATURAL]) && in_array($this->getStrength(), [self::PRIMARY, self::SECONDARY], true))
|
||||
{
|
||||
$ordinary_flags |= SORT_FLAG_CASE;
|
||||
}
|
||||
|
||||
if ($maintain_index_association)
|
||||
{
|
||||
asort($array, $ordinary_flags);
|
||||
}
|
||||
else
|
||||
{
|
||||
sort($array, $ordinary_flags);
|
||||
}
|
||||
|
||||
$locale_switcher->restore();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
// An interface for converting ICU datetime patterns to another format, eg strftime
|
||||
interface Formatter
|
||||
{
|
||||
// Convert an ICU pattern token into the nearest equivalent token.
|
||||
// Throws an exception if the token can't be converted.
|
||||
public function convert(string $token) : string;
|
||||
|
||||
public function escape(string $char) : string;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
class FormatterFlatpickr implements Formatter
|
||||
{
|
||||
// See https://flatpickr.js.org/formatting/
|
||||
private const FORMATTING_TOKENS = [
|
||||
'd', 'D', 'l', 'j', 'J', 'w', 'W', 'F', 'm', 'n', 'M',
|
||||
'U', 'y', 'Y', 'Z', 'H', 'h', 'G', 'i', 'S', 's', 'K'];
|
||||
|
||||
// Convert an ICU pattern token into the nearest equivalent token.
|
||||
// Throws an exception if the token can't be converted.
|
||||
public function convert(string $token) : string
|
||||
{
|
||||
switch ($token) {
|
||||
// AM or PM
|
||||
case 'a': // PM [abbrev]
|
||||
case 'aa': // PM [abbrev]
|
||||
case 'aaa': // PM [abbrev]
|
||||
case 'aaaa': // PM [wide]
|
||||
case 'aaaaa': // p
|
||||
// am, pm, noon, midnight
|
||||
case 'b': // mid.
|
||||
case 'bb': // mid.
|
||||
case 'bbb': // mid.
|
||||
case 'bbbb': // midnight
|
||||
case 'bbbbb': // md
|
||||
// flexible day periods
|
||||
case 'B': // at night [abbrev]
|
||||
case 'BB': // at night [abbrev]
|
||||
case 'BBB': // at night [abbrev]
|
||||
case 'BBBB': // at night [wide]
|
||||
case 'BBBBB': // at night [narrow]
|
||||
$format = 'K'; // AM/PM, eg AM or PM
|
||||
break;
|
||||
|
||||
// stand-alone local day of week
|
||||
case 'cccc': // Tuesday
|
||||
// day of week
|
||||
case 'EEEE': // Tuesday
|
||||
// local day of week
|
||||
case 'eeee': // Tuesday
|
||||
$format = 'l'; // A full textual representation of the day, eg Sunday through Saturday
|
||||
break;
|
||||
|
||||
// stand-alone local day of week
|
||||
case 'ccc': // Tue
|
||||
case 'ccccc': // T
|
||||
case 'cccccc': // Tu
|
||||
// day of week
|
||||
case 'E': // Tue
|
||||
case 'EE': // Tue
|
||||
case 'EEE': // Tue
|
||||
case 'EEEEE': // T
|
||||
case 'EEEEEE': // Tu
|
||||
// local day of week
|
||||
case 'eee': // Tue
|
||||
case 'eeeee': // T
|
||||
case 'eeeeee': // Tu
|
||||
$format = 'D'; // A textual representation of a day, eg Mon through Sun
|
||||
break;
|
||||
|
||||
// day in month
|
||||
case 'd': // 2
|
||||
$format = 'j'; // Day of the month without leading zeros, eg1 to 31
|
||||
break;
|
||||
|
||||
// day in month
|
||||
case 'dd': // 02
|
||||
$format = 'd'; // Day of the month, 2 digits with leading zeros, eg 01 to 31
|
||||
break;
|
||||
|
||||
// hour in day (0~23)
|
||||
case 'H': // 0
|
||||
// hour in day (0~23)
|
||||
case 'HH': // 00
|
||||
$format = 'H'; // Hours (24 hours), eg 00 to 23
|
||||
break;
|
||||
|
||||
// hour in am/pm (1~12)
|
||||
case 'h': // 7
|
||||
$format = 'h'; // Hours 1 to 12
|
||||
break;
|
||||
|
||||
// hour in am/pm (1~12)
|
||||
case 'hh': // 07
|
||||
$format = 'G'; // Hours, 2 digits with leading zeros 1 to 12
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'L': // 9
|
||||
// month in year
|
||||
case 'M': // 9
|
||||
$format = 'n'; // Numeric representation of a month, without leading zeros 1 through 12
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'LL': // 09
|
||||
// month in year
|
||||
case 'MM': // 09
|
||||
$format = 'm'; // Numeric representation of a month, with leading zero 01 through 12
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'LLL': // Sep
|
||||
// month in year
|
||||
case 'MMM': // Sep
|
||||
$format = 'M'; // A short textual representation of a month Jan through Dec
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'LLLL': // September
|
||||
// month in year
|
||||
case 'MMMM': // September
|
||||
$format = 'F'; // A full textual representation of a month January through December
|
||||
break;
|
||||
|
||||
// minute in hour
|
||||
case 'm': // 4
|
||||
$format = 'i'; // Minutes 00 to 59
|
||||
break;
|
||||
|
||||
// minute in hour
|
||||
case 'mm': // 04
|
||||
$format = 'i'; // Minutes 00 to 59
|
||||
break;
|
||||
|
||||
// second in minute
|
||||
case 's': // 5
|
||||
$format = 's'; // Seconds 0, 1 to 59
|
||||
break;
|
||||
|
||||
// second in minute
|
||||
case 'ss': // 05
|
||||
$format = 'S'; // Seconds, 2 digits 00 to 59
|
||||
break;
|
||||
|
||||
// week of year
|
||||
// The ICU documentation isn't very clear what is meant by "week of year", but it seems to be locale
|
||||
// dependent. In many locales it is the ISO week number, but in some locales it isn't. It (partly?)
|
||||
// depends on the locale's first day of the week, which can be got from IntlCalendar::getFirstDayOfWeek().
|
||||
case 'w': // 7
|
||||
case 'ww': // 07
|
||||
$format = 'W'; // Numeric representation of the week 0 (first week of the year) through 52 (last week of the year)
|
||||
break;
|
||||
|
||||
// year
|
||||
case 'y': // 1996
|
||||
case 'yyyy': // 1996
|
||||
$format = 'Y'; // A full numeric representation of a year, 4 digits, eg 1999 or 2003
|
||||
break;
|
||||
|
||||
// year
|
||||
case 'yy': // 96
|
||||
$format = 'y'; // A two digit representation of a year 99 or 03
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \MRBS\Exception("Could not convert '$token'");
|
||||
break;
|
||||
}
|
||||
|
||||
return $format;
|
||||
}
|
||||
|
||||
public function escape(string $char): string
|
||||
{
|
||||
if (in_array($char, self::FORMATTING_TOKENS))
|
||||
{
|
||||
return '\\\\' . $char;
|
||||
}
|
||||
|
||||
return $char;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
class FormatterStrftime implements Formatter
|
||||
{
|
||||
// Convert an ICU pattern token into the nearest equivalent token.
|
||||
// Throws an exception if the token can't be converted.
|
||||
public function convert(string $token) : string
|
||||
{
|
||||
switch ($token) {
|
||||
// AM or PM
|
||||
case 'a': // PM [abbrev]
|
||||
case 'aa': // PM [abbrev]
|
||||
case 'aaa': // PM [abbrev]
|
||||
case 'aaaa': // PM [wide]
|
||||
case 'aaaaa': // p
|
||||
// am, pm, noon, midnight
|
||||
case 'b': // mid.
|
||||
case 'bb': // mid.
|
||||
case 'bbb': // mid.
|
||||
case 'bbbb': // midnight
|
||||
case 'bbbbb': // md
|
||||
// flexible day periods
|
||||
case 'B': // at night [abbrev]
|
||||
case 'BB': // at night [abbrev]
|
||||
case 'BBB': // at night [abbrev]
|
||||
case 'BBBB': // at night [wide]
|
||||
case 'BBBBB': // at night [narrow]
|
||||
$format = '%P'; // lower-case 'am' or 'pm' based on the given time
|
||||
break;
|
||||
|
||||
// stand-alone local day of week
|
||||
case 'cccc': // Tuesday
|
||||
// day of week
|
||||
case 'EEEE': // Tuesday
|
||||
// local day of week
|
||||
case 'eeee': // Tuesday
|
||||
$format = '%A'; // A full textual representation of the day, eg Sunday through Saturday
|
||||
break;
|
||||
|
||||
// stand-alone local day of week
|
||||
case 'ccc': // Tue
|
||||
case 'ccccc': // T
|
||||
case 'cccccc': // Tu
|
||||
// day of week
|
||||
case 'E': // Tue
|
||||
case 'EE': // Tue
|
||||
case 'EEE': // Tue
|
||||
case 'EEEEE': // T
|
||||
case 'EEEEEE': // Tu
|
||||
// local day of week
|
||||
case 'eee': // Tue
|
||||
case 'eeeee': // T
|
||||
case 'eeeeee': // Tu
|
||||
$format = '%a'; // An abbreviated textual representation of the day, eg Sun through Sat
|
||||
break;
|
||||
|
||||
// day in month
|
||||
case 'd': // 2
|
||||
$format = '%i'; // One/two digit day of the month, eg 1 to 31
|
||||
break;
|
||||
|
||||
// day in month
|
||||
case 'dd': // 02
|
||||
$format = '%d'; // Two-digit day of the month (with leading zeros), eg 01 to 31
|
||||
break;
|
||||
|
||||
// day of year
|
||||
case 'D': // 189
|
||||
$format = '%E'; // Day of the year without leading zeroes
|
||||
break;
|
||||
|
||||
// hour in day (0~23)
|
||||
case 'H': // 0
|
||||
$format = '%k'; // Hour in 24-hour format, with a space preceding single digits, eg 0 through 23
|
||||
break;
|
||||
|
||||
// hour in day (0~23)
|
||||
case 'HH': // 00
|
||||
$format = '%H'; // Two digit representation of the hour in 24-hour format, eg 00 through 23
|
||||
break;
|
||||
|
||||
// hour in am/pm (1~12)
|
||||
case 'h': // 7
|
||||
$format = '%o'; // Hour in 12-hour format, with no space preceding single digits
|
||||
break;
|
||||
|
||||
// hour in am/pm (1~12)
|
||||
case 'hh': // 07
|
||||
$format = '%I'; // Two digit representation of the hour in 12-hour format, eg 01 through 12
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'L': // 9
|
||||
// month in year
|
||||
case 'M': // 9
|
||||
$format = '%f'; // One/two digit representation of the month, eg 1 (for January) through 12 (for December)
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'LL': // 09
|
||||
// month in year
|
||||
case 'MM': // 09
|
||||
$format = '%m'; // Two digit representation of the month, eg 01 (for January) through 12 (for December)
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'LLL': // Sep
|
||||
// month in year
|
||||
case 'MMM': // Sep
|
||||
$format = '%b'; // Abbreviated month name, based on the locale, eg Jan through Dec
|
||||
break;
|
||||
|
||||
// stand-alone month in year
|
||||
case 'LLLL': // September
|
||||
// month in year
|
||||
case 'MMMM': // September
|
||||
$format = '%B'; // Full month name, based on the locale, eg January through December
|
||||
break;
|
||||
|
||||
// minute in hour
|
||||
case 'm': // 4
|
||||
$format = '%q'; // Minute in the hour, with no leading zero
|
||||
break;
|
||||
|
||||
// minute in hour
|
||||
case 'mm': // 04
|
||||
$format = '%M'; // Minute in the hour, with leading zeroes
|
||||
break;
|
||||
|
||||
// second in minute
|
||||
case 's': // 5
|
||||
$format = '%v'; // Seconds, with no leading zeroes
|
||||
break;
|
||||
|
||||
// second in minute
|
||||
case 'ss': // 05
|
||||
$format = '%S'; // Two digit representation of the second, eg 00 through 59
|
||||
break;
|
||||
|
||||
// week of year
|
||||
// The ICU documentation isn't very clear what is meant by "week of year", but it seems to be locale
|
||||
// dependent. In many locales it is the ISO week number, but in some locales it isn't. It (partly?)
|
||||
// depends on the locale's first day of the week, which can be got from IntlCalendar::getFirstDayOfWeek().
|
||||
case 'w': // 7
|
||||
$format = '%J'; // ISO-8601:1988 week number of the given year without leading zeroes, eg 1 through 53
|
||||
break;
|
||||
|
||||
case 'ww': // 07
|
||||
$format = '%V'; // ISO-8601:1988 week number of the given year, eg 01 through 53
|
||||
break;
|
||||
|
||||
// year
|
||||
case 'y': // 1996
|
||||
case 'yyyy': // 1996
|
||||
$format = '%Y'; // Four digit representation for the year, eg 2038
|
||||
break;
|
||||
|
||||
// year
|
||||
case 'yy': // 96
|
||||
$format = '%y'; // Two digit representation of the year, eg 09 for 2009, 79 for 1979
|
||||
break;
|
||||
|
||||
// Time Zone: specific non-location
|
||||
case 'z': // PDT
|
||||
case 'zz': // PDT
|
||||
case 'zzz': // PDT
|
||||
case 'zzzz': // Pacific Daylight Time
|
||||
$format = '%Z'; // The time zone abbreviation, eg EST for Eastern Time
|
||||
break; // Windows: The %z and %Z modifiers both return the time zone name instead of the offset or abbreviation
|
||||
|
||||
default:
|
||||
throw new \MRBS\Exception("Could not convert '$token'");
|
||||
break;
|
||||
}
|
||||
|
||||
return $format;
|
||||
}
|
||||
|
||||
|
||||
public function escape(string $char): string
|
||||
{
|
||||
switch ($char)
|
||||
{
|
||||
case "\n":
|
||||
return '%n';
|
||||
break;
|
||||
case "\t":
|
||||
return '%t';
|
||||
break;
|
||||
case "%":
|
||||
return '%%';
|
||||
break;
|
||||
default:
|
||||
return $char;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// Note that we cannot use strict types here as some early versions of PHP (eg 7.2.34) throw
|
||||
// a TypeError if null is passed as the sixth parameter to \IntlDateFormatter::_construct(),
|
||||
// despite the signature on the manual page.
|
||||
// declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
// A factory class for creating either an instance of \IntlDateFormatter or \MRBS\Intl\IntlDateFormatter,
|
||||
// depending on the setting of the $force_strftime config variable.
|
||||
class IntlDateFormatterFactory
|
||||
{
|
||||
public static function create(
|
||||
?string $locale,
|
||||
int $dateType = \IntlDateFormatter::FULL,
|
||||
int $timeType = \IntlDateFormatter::FULL,
|
||||
$timezone = null,
|
||||
$calendar = null,
|
||||
?string $pattern = null)
|
||||
{
|
||||
global $force_strftime;
|
||||
|
||||
if ($force_strftime)
|
||||
{
|
||||
// This will return an instance of the emulation
|
||||
return new IntlDateFormatter($locale, $dateType, $timeType, $timezone, $calendar, $pattern);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This will return an instance of the standard PHP class if the 'intl' extension is loaded,
|
||||
// otherwise an instance of the emulation
|
||||
return new \IntlDateFormatter($locale, $dateType, $timeType, $timezone, $calendar, $pattern);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
class IntlDatePatternConverter
|
||||
{
|
||||
private const QUOTE_CHAR = "'";
|
||||
|
||||
private $formatter;
|
||||
|
||||
public function __construct(Formatter $formatter)
|
||||
{
|
||||
$this->formatter = $formatter;
|
||||
}
|
||||
|
||||
|
||||
public function convert(string $pattern) : string
|
||||
{
|
||||
// Parse the pattern
|
||||
// See https://unicode-org.github.io/icu/userguide/format_parse/datetime/
|
||||
// "Note: Any characters in the pattern that are not in the ranges of [‘a’..’z’] and
|
||||
// [‘A’..’Z’] will be treated as quoted text. For instance, characters like ':', '.',
|
||||
// ' ', '#' and '@' will appear in the resulting time text even they are not enclosed
|
||||
// within single quotes. The single quote is used to ‘escape’ letters. Two single
|
||||
// quotes in a row, whether inside or outside a quoted sequence, represent a ‘real’
|
||||
// single quote."
|
||||
$format = '';
|
||||
$token = '';
|
||||
$token_char = null;
|
||||
$in_quotes = false;
|
||||
// Split the string into an array of multibyte characters
|
||||
$chars = preg_split("//u", $pattern, 0, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
while (null !== ($char = array_shift($chars)))
|
||||
{
|
||||
$is_token_char = !$in_quotes && preg_match("/^[a-z]$/i", $char);
|
||||
if ($is_token_char)
|
||||
{
|
||||
// The start of a token
|
||||
if (!isset($token_char))
|
||||
{
|
||||
$token_char = $char;
|
||||
$token = $char;
|
||||
}
|
||||
// The continuation of a token
|
||||
elseif ($char === $token_char)
|
||||
{
|
||||
$token .= $char;
|
||||
}
|
||||
// The end of a token and the beginning of a new one
|
||||
else
|
||||
{
|
||||
$format .= $this->formatter->convert($token);
|
||||
$token_char = $char;
|
||||
$token = $char;
|
||||
}
|
||||
}
|
||||
// Check to see if a token has just ended, ie we've either got
|
||||
// a non-token character or there are no more characters left.
|
||||
if (($token !== '') && (!$is_token_char || empty($chars)))
|
||||
{
|
||||
$format .= $this->formatter->convert($token);
|
||||
$token = '';
|
||||
$token_char = null;
|
||||
}
|
||||
|
||||
// Quoted text
|
||||
if (!$is_token_char)
|
||||
{
|
||||
// If it's not a quote just add the character to the format
|
||||
if ($char !== self::QUOTE_CHAR)
|
||||
{
|
||||
$format .= $this->formatter->escape($char);
|
||||
}
|
||||
// Otherwise we have to work out whether the quote is the start or end of a
|
||||
// quoted sequence, or part of an escaped quote
|
||||
else
|
||||
{
|
||||
// Get the next character
|
||||
$char = array_shift($chars);
|
||||
if (isset($char))
|
||||
{
|
||||
// If it is a quote then it's an escaped quote and add it to the format
|
||||
if ($char === self::QUOTE_CHAR)
|
||||
{
|
||||
$format .= $this->formatter->escape($char);
|
||||
}
|
||||
// Otherwise it's either the start or end of a quoted section.
|
||||
// Toggle $in_quotes and add the character to the format if we're in quotes,
|
||||
// or else replace it so that it gets handled properly next time round.
|
||||
else
|
||||
{
|
||||
$in_quotes = !$in_quotes;
|
||||
if ($in_quotes)
|
||||
{
|
||||
$format .= $this->formatter->escape($char);
|
||||
}
|
||||
else
|
||||
{
|
||||
array_unshift($chars, $char);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $format;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
// A class which is just a wrapper for the standard PHP class if it's available; otherwise it
|
||||
// provides a basic emulation of PHP's IntlDatePatternGenerator class.
|
||||
|
||||
use MRBS\Exception;
|
||||
use MRBS\Language;
|
||||
|
||||
// We need to check that the 'intl' extension is loaded because earlier versions of
|
||||
// MRBS had the IntlDatePatternGenerator 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.
|
||||
if (class_exists('\IntlDatePatternGenerator') && extension_loaded('intl'))
|
||||
{
|
||||
class IntlDatePatternGenerator extends \IntlDatePatternGenerator
|
||||
{
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
class IntlDatePatternGenerator
|
||||
{
|
||||
private const DEFAULT_LOCALE = 'en';
|
||||
|
||||
private $locale;
|
||||
|
||||
// $locale The locale. If null is passed, uses the ini setting intl.default_locale.
|
||||
public function __construct(?string $locale = null)
|
||||
{
|
||||
if (!isset($locale)) {
|
||||
$locale = ini_get('intl.default_locale');
|
||||
if (($locale === false) || ($locale === '')) {
|
||||
throw new Exception("Could not get locale");
|
||||
}
|
||||
}
|
||||
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
|
||||
public function getBestPattern(string $skeleton)
|
||||
{
|
||||
$file = MRBS_ROOT . "/intl/skeletons/$skeleton.ini";
|
||||
|
||||
if (is_readable($file)) {
|
||||
$patterns = parse_ini_file($file);
|
||||
if (!empty($patterns)) {
|
||||
return $patterns[Language::convertToBcp47($this->locale)] ?? $patterns[self::DEFAULT_LOCALE] ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
class IntlException extends \Exception
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
/**
|
||||
* A wrapper for the Locale class, that uses the PHP method if available, otherwise falling
|
||||
* back to the emulator.
|
||||
*/
|
||||
class Locale
|
||||
{
|
||||
public const LANG_TAG = 'language';
|
||||
public const EXTLANG_TAG = 'extlang';
|
||||
public const SCRIPT_TAG = 'script';
|
||||
public const REGION_TAG = 'region';
|
||||
public const VARIANT_TAG = 'variant';
|
||||
public const GRANDFATHERED_LANG_TAG = 'grandfathered';
|
||||
public const PRIVATE_TAG = 'private';
|
||||
public const ACTUAL_LOCALE = 0;
|
||||
public const VALID_LOCALE = 1;
|
||||
|
||||
|
||||
public static function __callStatic(string $name, array $arguments)
|
||||
{
|
||||
// Use the PHP method if it exists, unless it's the acceptFromHttp() method. Until we stop using setlocale(), we
|
||||
// can't rely on PHP's acceptFromHttp() to tell us whether the locale is valid for setlocale() and have to use
|
||||
// the emulator, which does further checks.
|
||||
if (($name !== 'acceptFromHttp') && method_exists('\Locale', $name))
|
||||
{
|
||||
// Check that the method we're calling also exists in the emulator class, in case the 'intl' extension is not enabled.
|
||||
assert(method_exists(__NAMESPACE__ . '\LocaleEmulator', $name), "Call to \Locale::$name which hasn't been emulated.");
|
||||
return \Locale::$name(...$arguments);
|
||||
}
|
||||
|
||||
return LocaleEmulator::$name(...$arguments);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use MRBS\Language;
|
||||
use MRBS\System;
|
||||
|
||||
/**
|
||||
* A partial emulation of the PHP \Locale class.
|
||||
*
|
||||
* It is necessary because (a) the 'intl' extension isn't always enabled and (b) even if it is some
|
||||
* methods are only available in later versions of PHP, eg isRightToLeft is only available from PHP 8.5
|
||||
*/
|
||||
class LocaleEmulator
|
||||
{
|
||||
/**
|
||||
* A list of languages that use Right to Left text
|
||||
*/
|
||||
private const RTL_LANGUAGES = [
|
||||
// TODO: Expand this list
|
||||
'he'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Tries to find out best available locale based on HTTP "Accept-Language" header.
|
||||
*
|
||||
* @return string|false the corresponding locale identifier, or FALSE if none found
|
||||
*/
|
||||
public static function acceptFromHttp(string $header)
|
||||
{
|
||||
$accept_languages = self::toSortedArray($header);
|
||||
|
||||
foreach($accept_languages as $accept_language => $value)
|
||||
{
|
||||
if (System::isAvailableLocale($accept_language))
|
||||
{
|
||||
return $accept_language;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Returns a correctly ordered and delimited locale ID
|
||||
public static function composeLocale(array $subtags)
|
||||
{
|
||||
if (isset($subtags[Locale::GRANDFATHERED_LANG_TAG]))
|
||||
{
|
||||
return $subtags[Locale::GRANDFATHERED_LANG_TAG];
|
||||
}
|
||||
|
||||
$pieces = array();
|
||||
|
||||
foreach (array(Locale::LANG_TAG, Locale::EXTLANG_TAG, Locale::SCRIPT_TAG, Locale::REGION_TAG) as $subtag_label)
|
||||
{
|
||||
if (isset($subtags[$subtag_label]))
|
||||
{
|
||||
$pieces[] = $subtags[$subtag_label];
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(Locale::VARIANT_TAG . '0', $subtags))
|
||||
{
|
||||
for ($i=0; $i<15; $i++)
|
||||
{
|
||||
if (isset($subtags[Locale::VARIANT_TAG . $i]))
|
||||
{
|
||||
$pieces[] = $subtags[Locale::VARIANT_TAG . $i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists(Locale::PRIVATE_TAG . '0', $subtags))
|
||||
{
|
||||
$pieces[] = 'x';
|
||||
for ($i=0; $i<15; $i++)
|
||||
{
|
||||
if (isset($subtags[Locale::PRIVATE_TAG . $i]))
|
||||
{
|
||||
$pieces[] = $subtags[Locale::PRIVATE_TAG . $i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode('_', $pieces);
|
||||
}
|
||||
|
||||
|
||||
// Returns a key-value array of locale ID subtag elements.
|
||||
// Parses a language tag according to BCP 47
|
||||
// See http://tools.ietf.org/html/bcp47
|
||||
public static function parseLocale(string $locale) : ?array
|
||||
{
|
||||
static $regex = array('extlang' => '/^[[:alpha:]]{3}$/', // 3ALPHA
|
||||
'script' => '/^[[:alpha:]]{4}$/', // 4ALPHA
|
||||
'region' => '/^[[:alpha:]]{2}$|^[[:digit:]]{3}$/', // 2ALPHA or 3DIGIT
|
||||
'variant' => '/^[[:alnum:]]{5,8}$|^[[:digit:]][[:alnum:]]{3}$/'); // 5*8alphanum or (DIGIT 3alphanum)
|
||||
|
||||
static $grandfathered = array('en-GB-oed', // Irregular
|
||||
'i-ami',
|
||||
'i-bnn',
|
||||
'i-default',
|
||||
'i-enochian',
|
||||
'i-hak',
|
||||
'i-klingon',
|
||||
'i-lux',
|
||||
'i-mingo',
|
||||
'i-navajo',
|
||||
'i-pwn',
|
||||
'i-tao',
|
||||
'i-tay',
|
||||
'i-tsu',
|
||||
'sgn-BE-FR',
|
||||
'sgn-BE-NL',
|
||||
'sgn-CH-DE',
|
||||
'art-lojban', // Regular
|
||||
'cel-gaulish',
|
||||
'no-bok',
|
||||
'no-nyn',
|
||||
'zh-guoyu',
|
||||
'zh-hakka',
|
||||
'zh-min',
|
||||
'zh-min-nan',
|
||||
'zh-xiang');
|
||||
|
||||
// First check for a grandfathered tag
|
||||
if (isset($locale) && in_array($locale, $grandfathered))
|
||||
{
|
||||
return array(Locale::GRANDFATHERED_LANG_TAG => $locale);
|
||||
}
|
||||
|
||||
// Otherwise parse the subtags
|
||||
$result = array();
|
||||
|
||||
if (isset($locale))
|
||||
{
|
||||
$subtags = preg_split('/[-_]/', $locale);
|
||||
}
|
||||
else
|
||||
{
|
||||
$subtags = array();
|
||||
}
|
||||
|
||||
while (null !== ($subtag = array_shift($subtags)))
|
||||
{
|
||||
// Tags are case-insensitive, so convert to lowercase before processing and then
|
||||
// later convert as necessary according to convention
|
||||
$subtag = strtolower($subtag);
|
||||
|
||||
if ($subtag == 'x')
|
||||
{
|
||||
// If the subtag is an 'x' then everything else is a private subtag,
|
||||
// even if it occurs as the first subtag:
|
||||
// "The single-character subtag 'x' as the primary subtag indicates
|
||||
// that the language tag consists solely of subtags whose meaning is
|
||||
// defined by private agreement"
|
||||
$i = 0;
|
||||
while (null !== ($subtag = array_shift($subtags)))
|
||||
{
|
||||
$result[Locale::PRIVATE_TAG . $i] = $subtag;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
// The primary language subtag is the first subtag in a language tag and
|
||||
// cannot be omitted, with two exceptions:
|
||||
//
|
||||
// o The single-character subtag 'x' as the primary subtag ...
|
||||
// o The single-character subtag 'i' is used by some grandfathered tags ...
|
||||
elseif (!isset($result[Locale::LANG_TAG]))
|
||||
{
|
||||
// [ISO639-1] recommends that language codes be written in lowercase ('mn' Mongolian).
|
||||
// As the subtag will already be lowercase there's no need to do anything else
|
||||
$result[Locale::LANG_TAG] = $subtag;
|
||||
// Check if the next subtag looks like a language extension
|
||||
if (count($subtags) > 0)
|
||||
{
|
||||
if (preg_match($regex['extlang'], $subtags[0]))
|
||||
{
|
||||
$result[Locale::EXTLANG_TAG] = strtolower(array_shift($subtags));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Script
|
||||
elseif (preg_match($regex['script'], $subtag))
|
||||
{
|
||||
// [ISO15924] recommends that script codes use lowercase with the
|
||||
// initial letter capitalized ('Cyrl' Cyrillic).
|
||||
$result[Locale::SCRIPT_TAG] = ucfirst($subtag);
|
||||
}
|
||||
|
||||
// Region
|
||||
elseif (preg_match($regex['region'], $subtag))
|
||||
{
|
||||
// [ISO3166-1] recommends that country codes be capitalized ('MN'
|
||||
// Mongolia).
|
||||
$result[Locale::REGION_TAG] = strtoupper($subtag);
|
||||
}
|
||||
|
||||
// Variants
|
||||
elseif (preg_match($regex['variant'], $subtag))
|
||||
{
|
||||
$i = 0;
|
||||
do
|
||||
{
|
||||
// If the subtag doesn't look like a variant then we've got them all
|
||||
// and gone one subtag too far, so put it back
|
||||
if (!preg_match($regex['variant'], $subtag))
|
||||
{
|
||||
array_unshift($subtags, $subtag);
|
||||
break;
|
||||
}
|
||||
$result[Locale::VARIANT_TAG . $i] = $subtag;
|
||||
}
|
||||
while (null !== ($subtag = array_shift($subtags)));
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
trigger_error("parseLocale: could not parse subtag '$subtag'", E_USER_NOTICE);
|
||||
// This is how the PHP version behaves: if it can't parse the locale completely
|
||||
// it returns an empty array.
|
||||
$result = array();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public static function isRightToLeft(string $locale): bool
|
||||
{
|
||||
return in_array(mb_strtolower($locale), self::RTL_LANGUAGES);
|
||||
}
|
||||
|
||||
|
||||
// Searches the items in the array $langtag for the best match to the language range
|
||||
// specified in $locale according to RFC 4647's lookup algorithm. The langtags and
|
||||
// locale can have subtags separated by '-' or '_' and the search is case-insensitive.
|
||||
// Charsets (eg '.UTF-8') are stripped off $locale
|
||||
//
|
||||
// Returns the best match, or else an empty string if no match
|
||||
public static function lookup(array $langtag, string $locale, bool $canonicalize=false, ?string $default=null) : ?string
|
||||
{
|
||||
if ($canonicalize)
|
||||
{
|
||||
throw new InvalidArgumentException('MRBS: the MRBS version of Locale::lookup() does not yet support $canonicalize = true');
|
||||
}
|
||||
|
||||
// Get the langtags and locale in the same format, ie separated by '-' and
|
||||
// all lower case
|
||||
$standard_langtags = self::standardise($langtag);
|
||||
// Strip off any charset (eg '.UTF-8');
|
||||
$locale = preg_replace('/\..*$/', '', $locale);
|
||||
$standard_locale = self::standardise($locale);
|
||||
|
||||
// Look for a match. If there isn't one remove the last subtag from the end
|
||||
// of the locale and try again.
|
||||
while (false === ($index = array_search($standard_locale, $standard_langtags)))
|
||||
{
|
||||
if (false === ($pos = strrpos($standard_locale, '-')))
|
||||
{
|
||||
return (isset($default)) ? $default : '';
|
||||
}
|
||||
$standard_locale = substr($standard_locale, 0, $pos);
|
||||
}
|
||||
|
||||
return $langtag[$index]; // Return the match in its original format
|
||||
}
|
||||
|
||||
|
||||
// Converts $langtag, which can be a string or an array, into a standard form with
|
||||
// subtags all in lower case and separated by '-';
|
||||
private static function standardise($langtag)
|
||||
{
|
||||
$glue = ',';
|
||||
$result = (is_array($langtag)) ? implode($glue, $langtag) : $langtag;
|
||||
$result = mb_strtolower(str_replace('_', '-', $result));
|
||||
return (is_array($langtag)) ? explode($glue, $result) : $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts an Accept-Language request-header to an array of acceptable languages
|
||||
* with the language as the key and the quality value as the value, sorted in
|
||||
* decreasing order of quality value. A wildcard in the header is translated.
|
||||
*
|
||||
* @return array<string, float>
|
||||
*/
|
||||
private static function toSortedArray(string $header) : array
|
||||
{
|
||||
return Language::getQualifiers($header, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
use MRBS\Language;
|
||||
use MRBS\System;
|
||||
|
||||
class LocaleSwitcher
|
||||
{
|
||||
private $category;
|
||||
private $locale;
|
||||
private $old_locale;
|
||||
|
||||
|
||||
public function __construct(int $category, string $locale)
|
||||
{
|
||||
$this->category = $category;
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
|
||||
public function switch()
|
||||
{
|
||||
// It's not worth testing whether the new locale is the same as the old one, thereby saving us setting
|
||||
// the locale again. That's because setting the locale on Unix systems seems to be about 100 times
|
||||
// faster than on Windows. So on Unix systems, it's not worth worrying about. And on Windows, we have
|
||||
// to set the locale again anyway 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()."
|
||||
$this->old_locale = setlocale($this->category, '0');
|
||||
Language::setLocale($this->category, $this->locale);
|
||||
}
|
||||
|
||||
|
||||
public function restore()
|
||||
{
|
||||
if (!isset($this->old_locale))
|
||||
{
|
||||
throw new \RuntimeException("switch() must be called before restore().");
|
||||
}
|
||||
|
||||
if (false === setlocale($this->category, $this->old_locale))
|
||||
{
|
||||
// Shouldn't happen as the old locale was what the system told us it was.
|
||||
throw new \RuntimeException("Could not restore locale to '" . $this->old_locale . "'");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Intl;
|
||||
|
||||
|
||||
/**
|
||||
* A partial, very basic, emulation of the PHP \NumberFormatter class. Some methods are not implemented,
|
||||
* and some attributes are not supported. This class is only intended as a very basic fallback if the
|
||||
* intl extension is not available. For best results use the intl extension.
|
||||
* @see \NumberFormatter
|
||||
*/
|
||||
class NumberFormatter
|
||||
{
|
||||
public const PATTERN_DECIMAL = 0;
|
||||
public const DECIMAL = 1;
|
||||
public const CURRENCY = 2;
|
||||
public const PERCENT = 3;
|
||||
public const SCIENTIFIC = 4;
|
||||
public const SPELLOUT = 5;
|
||||
public const ORDINAL = 6;
|
||||
public const DURATION = 7;
|
||||
public const PATTERN_RULEBASED = 9;
|
||||
public const IGNORE = 0;
|
||||
public const CURRENCY_ACCOUNTING = 12;
|
||||
public const DECIMAL_COMPACT_SHORT = 14; // PHP 8.5 onwards
|
||||
public const DECIMAL_COMPACT_LONG= 15; // PHP 8.5 onwards
|
||||
public const DEFAULT_STYLE = 1;
|
||||
public const ROUND_CEILING = 0;
|
||||
public const ROUND_FLOOR = 1;
|
||||
public const ROUND_DOWN = 2;
|
||||
public const ROUND_UP = 3;
|
||||
public const ROUND_TOWARD_ZERO = 2;
|
||||
public const ROUND_AWAY_FROM_ZERO = 3;
|
||||
public const ROUND_HALFEVEN = 4;
|
||||
public const ROUND_HALFODD = 8;
|
||||
public const ROUND_HALFDOWN = 5;
|
||||
public const ROUND_HALFUP = 6;
|
||||
public const PAD_BEFORE_PREFIX = 0;
|
||||
public const PAD_AFTER_PREFIX = 1;
|
||||
public const PAD_BEFORE_SUFFIX = 2;
|
||||
public const PAD_AFTER_SUFFIX = 3;
|
||||
public const PARSE_INT_ONLY = 0;
|
||||
public const GROUPING_USED = 1;
|
||||
public const DECIMAL_ALWAYS_SHOWN = 2;
|
||||
public const MAX_INTEGER_DIGITS = 3;
|
||||
public const MIN_INTEGER_DIGITS = 4;
|
||||
public const INTEGER_DIGITS = 5;
|
||||
public const MAX_FRACTION_DIGITS = 6;
|
||||
public const MIN_FRACTION_DIGITS = 7;
|
||||
public const FRACTION_DIGITS = 8;
|
||||
public const MULTIPLIER = 9;
|
||||
public const GROUPING_SIZE = 10;
|
||||
public const ROUNDING_MODE = 11;
|
||||
public const ROUNDING_INCREMENT = 12;
|
||||
public const FORMAT_WIDTH = 13;
|
||||
public const PADDING_POSITION = 14;
|
||||
public const SECONDARY_GROUPING_SIZE = 15;
|
||||
public const SIGNIFICANT_DIGITS_USED = 16;
|
||||
public const MIN_SIGNIFICANT_DIGITS = 17;
|
||||
public const MAX_SIGNIFICANT_DIGITS = 18;
|
||||
public const LENIENT_PARSE = 19;
|
||||
public const POSITIVE_PREFIX = 0;
|
||||
public const POSITIVE_SUFFIX = 1;
|
||||
public const NEGATIVE_PREFIX = 2;
|
||||
public const NEGATIVE_SUFFIX = 3;
|
||||
public const PADDING_CHARACTER = 4;
|
||||
public const CURRENCY_CODE = 5;
|
||||
public const DEFAULT_RULESET = 6;
|
||||
public const PUBLIC_RULESETS = 7;
|
||||
public const DECIMAL_SEPARATOR_SYMBOL = 0;
|
||||
public const GROUPING_SEPARATOR_SYMBOL = 1;
|
||||
public const PATTERN_SEPARATOR_SYMBOL = 2;
|
||||
public const PERCENT_SYMBOL = 3;
|
||||
public const ZERO_DIGIT_SYMBOL = 4;
|
||||
public const DIGIT_SYMBOL = 5;
|
||||
public const MINUS_SIGN_SYMBOL = 6;
|
||||
public const PLUS_SIGN_SYMBOL = 7;
|
||||
public const CURRENCY_SYMBOL = 8;
|
||||
public const INTL_CURRENCY_SYMBOL = 9;
|
||||
public const MONETARY_SEPARATOR_SYMBOL = 10;
|
||||
public const EXPONENTIAL_SYMBOL = 11;
|
||||
public const PERMILL_SYMBOL = 12;
|
||||
public const PAD_ESCAPE_SYMBOL = 13;
|
||||
public const INFINITY_SYMBOL = 14;
|
||||
public const NAN_SYMBOL = 15;
|
||||
public const SIGNIFICANT_DIGIT_SYMBOL = 16;
|
||||
public const MONETARY_GROUPING_SEPARATOR_SYMBOL = 17;
|
||||
public const TYPE_DEFAULT = 0;
|
||||
public const TYPE_INT32 = 1;
|
||||
public const TYPE_INT64 = 2;
|
||||
public const TYPE_DOUBLE = 3;
|
||||
public const TYPE_CURRENCY = 4;
|
||||
public const CURRENCY_ISO = 10; // PHP 8.5 onwards
|
||||
public const CURRENCY_PLURAL = 11; // PHP 8.5 onwards
|
||||
public const CASH_CURRENCY = 13; // PHP 8.5 onwards
|
||||
public const CURRENCY_STANDARD = 16; // PHP 8.5 onwards
|
||||
|
||||
public const VALID_ATTRIBUTES = [
|
||||
self::PARSE_INT_ONLY,
|
||||
self::GROUPING_USED,
|
||||
self::DECIMAL_ALWAYS_SHOWN,
|
||||
self::MAX_INTEGER_DIGITS,
|
||||
self::MIN_INTEGER_DIGITS,
|
||||
self::INTEGER_DIGITS,
|
||||
self::MAX_FRACTION_DIGITS,
|
||||
self::MIN_FRACTION_DIGITS,
|
||||
self::FRACTION_DIGITS,
|
||||
self::MULTIPLIER,
|
||||
self::GROUPING_SIZE,
|
||||
self::ROUNDING_MODE,
|
||||
self::ROUNDING_INCREMENT,
|
||||
self::FORMAT_WIDTH,
|
||||
self::PADDING_POSITION,
|
||||
self::SECONDARY_GROUPING_SIZE,
|
||||
self::SIGNIFICANT_DIGITS_USED,
|
||||
self::MIN_SIGNIFICANT_DIGITS,
|
||||
self::MAX_SIGNIFICANT_DIGITS,
|
||||
self::LENIENT_PARSE
|
||||
];
|
||||
|
||||
public const VALID_STYLES = [
|
||||
self::PATTERN_DECIMAL,
|
||||
self::DECIMAL,
|
||||
self::CURRENCY,
|
||||
self::PERCENT,
|
||||
self::SCIENTIFIC,
|
||||
self::SPELLOUT,
|
||||
self::ORDINAL,
|
||||
self::DURATION,
|
||||
self::PATTERN_RULEBASED,
|
||||
self::CURRENCY_ACCOUNTING,
|
||||
self::DEFAULT_STYLE,
|
||||
self::IGNORE
|
||||
];
|
||||
|
||||
public const VALID_TEXT_ATTRIBUTES = [
|
||||
self::POSITIVE_PREFIX,
|
||||
self::POSITIVE_SUFFIX,
|
||||
self::NEGATIVE_PREFIX,
|
||||
self::NEGATIVE_SUFFIX,
|
||||
self::PADDING_CHARACTER,
|
||||
self::CURRENCY_CODE,
|
||||
self::DEFAULT_RULESET,
|
||||
self::PUBLIC_RULESETS
|
||||
];
|
||||
|
||||
private $attributes;
|
||||
private $locale;
|
||||
private $style;
|
||||
private $text_attributes;
|
||||
|
||||
/**
|
||||
* @see \NumberFormatter::__construct()
|
||||
*/
|
||||
public function __construct(string $locale, int $style, ?string $pattern = null)
|
||||
{
|
||||
$this->locale = $locale;
|
||||
|
||||
if (!in_array($style, self::VALID_STYLES, true))
|
||||
{
|
||||
throw new \IntlException(str_replace(__NAMESPACE__ . '\\', '', __METHOD__) . '(): number formatter creation failed');
|
||||
}
|
||||
$this->style = $style;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \NumberFormatter::format()
|
||||
*/
|
||||
public function format($num, int $type = self::TYPE_DEFAULT)
|
||||
{
|
||||
$locale_switcher = new LocaleSwitcher(LC_NUMERIC, $this->locale);
|
||||
$locale_switcher->switch();
|
||||
|
||||
$locale_info = localeconv();
|
||||
|
||||
$locale_switcher->restore();
|
||||
return number_format($num, 0, $locale_info['decimal_point'], $locale_info['thousands_sep']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \NumberFormatter::setAttribute()
|
||||
*/
|
||||
public function setAttribute(int $attribute, $value): bool
|
||||
{
|
||||
if (!in_array($attribute, self::VALID_ATTRIBUTES, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->attributes[$attribute] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see \NumberFormatter::setTextAttribute()
|
||||
*/
|
||||
public function setTextAttribute(int $attribute, string $value): bool
|
||||
{
|
||||
if (!in_array($attribute, self::VALID_TEXT_ATTRIBUTES, true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->text_attributes[$attribute] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user