Files
人事系统开发 1ba6efd8ed 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 排除,勿推送到公开仓库。
2026-09-09 16:55:02 +08:00

467 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace MRBS;
use InvalidArgumentException;
use MRBS\ICalendar\Property;
use MRBS\ICalendar\RFC5545;
class RepeatRule
{
// Repeat types
public const NONE = 0;
public const DAILY = 1;
public const WEEKLY = 2;
public const MONTHLY = 3;
public const YEARLY = 4;
public const REPEAT_TYPES = [
self::NONE,
self::DAILY,
self::WEEKLY,
self::MONTHLY,
self::YEARLY
];
// Monthly repeat types
public const MONTHLY_ABSOLUTE = 0;
public const MONTHLY_RELATIVE = 1;
public const MONTHLY_TYPES = [
self::MONTHLY_ABSOLUTE,
self::MONTHLY_RELATIVE
];
private $days = []; // An array of days for weekly repeats; 0 for Sunday, etc.
private $end_date; // The repeat end date. A DateTime object
private $interval; // The repeat interval
private $monthly_absolute; // The absolute day of the month to repeat on for monthly repeats
private $monthly_relative; // The relative day of the month to repeat on for monthly repeats
private $monthly_type; // The monthly repeat type (Absolute or relative). An int.
private $type; // The repeat type (NONE, DAILY, etc.). An int.
public function getDays() : array
{
return $this->days;
}
// Returns a list of repeat days as a string (eg "Thursday Friday"),
// sorted in order and starting at $weekstarts
public function getDaysAsNames(bool $for_mail=false) : string
{
global $weekstarts;
$names = array();
$locale = ($for_mail) ? Language::getInstance()->getMailLocale() : null;
for ($i=0; $i<DAYS_PER_WEEK; $i++)
{
$d = ($i + $weekstarts) % DAYS_PER_WEEK;
if (in_array($d, $this->getDays()))
{
$names[] = day_name($d, null, $locale);
}
}
return implode(' ', $names);
}
public function getEndDate() : ?DateTime
{
return $this->end_date ?? null;
}
public function getInterval() : ?int
{
return $this->interval ?? null;
}
// Returns the interval units, in the local language. If $for_mail
// is set the mail language is used.
public function getIntervalUnits(bool $for_mail=false) : string
{
$repeat_type = $this->getType();
$repeat_interval = $this->getInterval();
if ($for_mail)
{
switch ($repeat_type)
{
case RepeatRule::DAILY:
return ($repeat_interval == 1) ? get_mail_vocab('day') : get_mail_vocab('days');
break;
case RepeatRule::WEEKLY:
return ($repeat_interval == 1) ? get_mail_vocab('week') : get_mail_vocab('weeks');
break;
case RepeatRule::MONTHLY:
return ($repeat_interval == 1) ? get_mail_vocab('month') : get_mail_vocab('months');
break;
case RepeatRule::YEARLY:
return ($repeat_interval == 1) ? get_mail_vocab('year_lc') : get_mail_vocab('years');
break;
default:
return '';
break;
}
}
switch ($repeat_type)
{
case RepeatRule::DAILY:
return ($repeat_interval == 1) ? get_vocab('day') : get_vocab('days');
break;
case RepeatRule::WEEKLY:
return ($repeat_interval == 1) ? get_vocab('week') : get_vocab('weeks');
break;
case RepeatRule::MONTHLY:
return ($repeat_interval == 1) ? get_vocab('month') : get_vocab('months');
break;
case RepeatRule::YEARLY:
return ($repeat_interval == 1) ? get_vocab('year_lc') : get_vocab('years');
break;
default:
return '';
break;
}
}
public function getMonthlyAbsolute() : ?int
{
return $this->monthly_absolute ?? null;
}
public function getMonthlyRelative() : ?string
{
return $this->monthly_relative ?? null;
}
public function getMonthlyType() : ?int
{
return $this->monthly_type ?? null;
}
// Returns the repeat days encoded as a 7 character string for use in the database
public function getRepOpt() : string
{
$result = '';
for ($i = 0; $i < DAYS_PER_WEEK; $i++)
{
$result .= in_array($i, $this->days) ? '1' : '0';
}
return $result;
}
public function getType() : int
{
return $this->type;
}
public function setDays(array $days) : void
{
foreach ($days as $day)
{
// Cast to int
$this->days[] = (int) $day;
// Check it's a valid day
if (($day < 0) || ($day > 6))
{
throw new InvalidArgumentException("Invalid day of the week '$day'");
}
}
// We need the repeat days to be in order
sort($this->days);
}
public function setDaysFromOpt(string $rep_opt) : void
{
$days = [];
for ($i=0; $i<DAYS_PER_WEEK; $i++)
{
if (isset($rep_opt[$i]) && $rep_opt[$i])
{
$days[] = $i;
}
}
$this->setDays($days);
}
// Set the repeat days given an array of RFC5545 days, eg ['MO', 'TH'].
public function setDaysFromRFC5545(array $rfc5545_days) : void
{
$days = [];
foreach ($rfc5545_days as $rfc5545_day)
{
$days[] = RFC5545::convertDayToOrd($rfc5545_day);
}
$this->setDays($days);
}
public function setEndDate(?DateTime $end_date) : void
{
$this->end_date = $end_date;
}
public function setInterval(int $interval) : void
{
$this->interval = $interval;
}
public function setMonthlyAbsolute(?int $absolute) : void
{
$this->monthly_absolute = $absolute;
}
public function setMonthlyRelative(?string $relative) : void
{
$this->monthly_relative = $relative;
}
public function setMonthlyType(?int $monthly_type) : void
{
if (!in_array($monthly_type, self::MONTHLY_TYPES))
{
throw new InvalidArgumentException("Invalid monthly type '$monthly_type'");
}
$this->monthly_type = $monthly_type;
}
public function setType(int $type) : void
{
if (!in_array($type, self::REPEAT_TYPES))
{
throw new InvalidArgumentException("Invalid repeat type '$type'");
}
$this->type = $type;
}
// Returns an array of start times for the entries in this series given a start time
// for the beginning of the series. Optionally limited to $limit entries.
public function getRepeatStartTimes(int $start_time, ?int $limit=null) : array
{
$entries = array();
$date = new DateTime();
$date->setTimestamp($start_time);
// Make sure that the first date is a member of the series
switch($this->getType())
{
case self::WEEKLY:
$repeat_days = $this->getDays();
$n_repeat_days = count($repeat_days); // We will need this later
if ($n_repeat_days == 0)
{
throw new Exception("No weekly repeat days specified in repeat rule");
}
while (!in_array($date->format('w'), $repeat_days))
{
// The hour will be preserved across DST transitions
$date->modify('+1 day');
}
$start_dow = $date->format('w'); // We will need this later
$start_index = array_search($start_dow, $repeat_days); // We will need this later
break;
case self::MONTHLY:
if ($this->getMonthlyType() == self::MONTHLY_ABSOLUTE)
{
if ($date->getDay() != $this->getMonthlyAbsolute())
{
if ($date->getDay() > $this->getMonthlyAbsolute())
{
$date->modify('+1 month');
}
$date->setDayNoOverflow($this->getMonthlyAbsolute());
}
}
else // must be relative
{
// Advance to a month that has this relative date. For example, not
// every month will have a '5SU' (fifth Sunday)
while (false === $date->setRelativeDay($this->getMonthlyRelative()))
{
$date->modify('+1 month');
}
}
break;
case self::YEARLY:
$start_day = $date->getDay(); // We will need this later
break;
default:
break;
}
// Now get the entry start times
$i = 0;
// TODO: check end_date condition
while ((!isset($limit) || ($i < $limit)) && ($date <= $this->getEndDate()))
{
// Add this start date to the result and increment the counter
$i++;
$entries[] = $date->getTimestamp();
// Advance to the next entry
switch ($this->getType())
{
case self::DAILY:
$modifier = '+' . $this->getInterval() . 'days';
$date->modify($modifier);
break;
case self::WEEKLY:
$delta_weeks = $this->getInterval();
// If there is more than one repeat day then advance to the next repeat day
if ($n_repeat_days > 1)
{
// Get the next repeat day
$current_index = array_search($date->format('w'), $repeat_days);
$next_index = ($current_index + 1) % $n_repeat_days;
// Advance to it
$modifier = '+' . (($repeat_days[$next_index] + DAYS_PER_WEEK - $repeat_days[$current_index]) % DAYS_PER_WEEK) . 'days';
$date->modify($modifier);
// If we're back to the start day then we need to advance by the interval less a week.
// Otherwise, we don't need to do anything more
$delta_weeks = ($next_index == $start_index) ? $delta_weeks - 1 : 0;
}
// Advance by the required number of weeks
if ($delta_weeks != 0)
{
$modifier = '+' . $delta_weeks . ' weeks';
$date->modify($modifier);
}
break;
case self::MONTHLY:
// Move the date forward by the interval number of months
$date->modifyMonthsNoOverflow($this->getInterval(), true);
// If it's an absolute date then set the day again, in case previously the date
// was moved back to the end of the month.
if ($this->getMonthlyType() == self::MONTHLY_ABSOLUTE)
{
$date->setDayNoOverflow($this->getMonthlyAbsolute());
}
// If it's a relative date then set the new relative date in the first month that's
// got one.
else
{
while (false === $date->setRelativeDay($this->getMonthlyRelative()))
{
$date->modifyMonthsNoOverflow($this->getInterval(), true);
}
}
break;
case self::YEARLY:
// Move the year forward by the interval number of years
$date->modifyYearsNoOverflow($this->getInterval(), true);
// Get the day of the month back to where it should be (in case we
// decremented it to make it a valid date last time round)
$date->setDayNoOverflow($start_day);
break;
}
}
return $entries;
}
// TODO: remove temporary code when the transition to RepeatRule is complete
// Temporary method while we transition the code to using RepeatRule.
// Converts a new style $data into one with old style repeat fields
public static function fixUp(array $data) : array
{
$result = $data;
if (isset($data['repeat_rule']))
{
$repeat_rule = $data['repeat_rule'];
$result['rep_type'] = $repeat_rule->getType();
$result['rep_interval'] = $repeat_rule->getInterval();
$repeat_end_date = $repeat_rule->getEndDate();
$result['end_date'] = (isset($repeat_end_date)) ? $repeat_end_date->getTimestamp() : null;
$result['rep_opt'] = $repeat_rule->getRepOpt();
$result['monthly_absolute'] = $repeat_rule->getMonthlyAbsolute();
$result['monthly_relative'] = $repeat_rule->getMonthlyRelative();
}
return $result;
}
// Converts a repeat rule to an RFC5545 recurrence rule (RRULE)
public function toRFC5545Rule() : string
{
$rule = '';
if ($this->getType() == self::NONE)
{
return $rule;
}
switch($this->getType())
{
case self::DAILY:
$rule .= "FREQ=DAILY";
break;
case self::WEEKLY:
$rule .= "FREQ=WEEKLY";
// Get the repeat days of the week
$days_of_week = array();
foreach ($this->getDays() as $day)
{
$days_of_week[] = RFC5545::DAYS[$day];
}
$dow_list = implode(',', $days_of_week);
if (!empty($dow_list))
{
$rule .= ";BYDAY=$dow_list";
}
break;
case self::MONTHLY:
$rule .= "FREQ=MONTHLY";
if ($this->getMonthlyType() == self::MONTHLY_ABSOLUTE)
{
$rule .= ";BYMONTHDAY=" . $this->getMonthlyAbsolute();
}
else
{
$rule .= ";BYDAY=" . $this->getMonthlyRelative();
}
break;
case self::YEARLY:
$rule .= "FREQ=YEARLY";
break;
}
// The interval (if necessary)
if ($this->getInterval() > 1)
{
$rule .= ";INTERVAL=" . $this->getInterval();
}
// The UNTIL date-time "MUST be specified in UTC time"
$rule .= ";UNTIL=" . Property::convertTimestamps($this->getEndDate()->getTimestamp())[0];
return $rule;
}
}