Files
mrbs-equbao-2026/web/lib/MRBS/Periods.php
T
人事系统开发 48092cab42
Docker image / push (push) Canceled after 0s
MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、登录页JS修复等全部改动
2026-09-08 21:19:47 +08:00

348 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace MRBS;
use Countable;
use DateTimeZone;
use SeekableIterator;
/**
* A class for handling a list of periods for a given area. It isolates from the rest of MRBS
* the knowledge of how periods are represented in start and end times and how they are stored
* in the database.
*
* Periods are represented as minutes from noon, ie 1200 is $period[0], 1201 $period[1], etc.
*
* They are stored in the database as JSON encoded arrays of period names and start and end times.
*/
class Periods implements Countable, SeekableIterator
{
private $area_id;
private $tzid;
private $index = 0;
private $data = [];
/**
* @param int|null $area_id The area ID, or null if not yet known. Creating an instance of this class without
* an area can be useful when creating a new area.
*/
public function __construct(?int $area_id = null)
{
$this->area_id = $area_id;
if (isset($area_id))
{
$this->tzid = get_area_timezone($area_id);
}
}
/**
* Get the periods for a given area from the database.
*/
public static function getForArea(int $area_id) : self
{
static $result = []; // Cache for performance
if (!isset($result[$area_id]))
{
$sql = "SELECT periods
FROM " . _tbl('area') . "
WHERE id=:id
LIMIT 1";
$res = db()->query($sql, [':id' => $area_id]);
if (false !== ($row = $res->next_row_keyed()))
{
$result[$area_id] = self::fromDbValue($area_id, $row['periods']);
}
}
return clone $result[$area_id];
}
/**
* Convert a periods value as stored in the database to an instance of this class.
*/
public static function fromDbValue(int $area_id, string $value) : self
{
$result = new self($area_id);
$array = json_decode($value, true);
// The periods are stored in the database as either:
// (a) a simple array of period names (the old way of storing periods
// which we handle for backwards compatibility); or
// (b) an associative array of period names and start/end times.
if (is_assoc($array))
{
foreach ($array as $period_name => $times)
{
$result->add(new Period($period_name, $times[0], $times[1]));
}
}
else
{
foreach ($array as $period_name)
{
$result->add(new Period($period_name));
}
}
return $result;
}
/**
* Convert the object to a value suitable for storing in the database.
*/
public function toDbValue() : string
{
$result = [];
foreach ($this->data as $period)
{
$result[$period->name] = [$period->start, $period->end];
}
return json_encode($result);
}
/**
* Validate the periods, checking that the start and end times are valid and in ascending order.
*
* @return true|string
*/
public function validate()
{
foreach ($this->data as $i => $period)
{
// If we're not using times, everything is OK.
if (($i === 0) && (!isset($period->start)))
{
return true;
}
// Otherwise check that the start and end times are valid and are in ascending order.
try
{
if (false === ($start = DateTime::createFromFormat('H:i', $period->start)))
{
return get_vocab('invalid_period_start_time', $period->start, $period->name);
}
if (false === ($end = DateTime::createFromFormat('H:i', $period->end)))
{
return get_vocab('invalid_period_end_time', $period->end, $period->name);
}
if (isset($last_end_time) && ($start < $last_end_time))
{
return get_vocab('period_start_before_last_end', $period->name);
}
if ($start >= $end)
{
return get_vocab('period_must_have_positive_duration', $period->name);
}
$last_end_time = $end;
}
catch (\Exception $e)
{
return get_vocab('invalid_period_time', $period->name);
}
}
return true;
}
public function add(Period $period) : void
{
$this->data[] = $period;
}
public function current() : Period
{
return $this->data[$this->index];
}
public function next() : void
{
$this->index++;
}
public function key(): int
{
return $this->index;
}
public function valid(): bool
{
return isset($this->data[$this->index]);
}
public function rewind() : void
{
$this->index = 0;
}
public function count() : int
{
return count($this->data);
}
public function seek($offset) : void
{
if ($offset < 0 || $offset >= $this->count())
{
throw new \OutOfBoundsException("Invalid offset $offset");
}
$this->index = $offset;
}
/**
* Check whether the object has start and end times defined.
*/
public function hasTimes() : bool
{
return ($this->count() > 0) && ($this->data[0]->start !== null);
}
/**
* Get a period by its index. Note that the iterator key is unchanged.
*/
public function offsetGet($offset) : Period
{
if ($offset < 0 || $offset >= $this->count())
{
throw new \OutOfBoundsException("Invalid offset $offset");
}
return $this->data[$offset];
}
/**
* Get a period by its nominal start time in seconds. Note that the
* iterator key is unchanged.
*/
public function offsetGetByNominalSeconds(int $seconds) : Period
{
return $this->offsetGet(self::nominalSecondsToIndex($seconds));
}
/**
* Get a period by its timestamp. Note that the iterator key is unchanged.
*
* @param bool $previous If true, return the previous period, otherwise return this period.
*/
public function offsetGetByTimestamp(int $timestamp, bool $previous=false) : Period
{
$index = $this->timestampToIndex($timestamp);
if ($previous)
{
$index--;
}
// Make sure we're within bounds
return $this->offsetGet(max(0, min($index, $this->count()-1)));
}
public static function getHourByOffset(int $offset) : int
{
return 12 + intval($offset/60);
}
public static function getMinuteByOffset(int $offset) : int
{
return $offset % 60;
}
/**
* Get the timestamp, as held in the entry table, of the start of the period at the given index on the given date.
*/
public function getStartTimestamp(int $index, DateTime $date) : int
{
return $date->setTime(12, 0)->getTimestamp() + ($index * 60);
}
/**
* Get the timestamp, as held in the entry table, of the end of the period at the given index on the given date.
*/
public function getEndTimestamp(int $index, DateTime $date) : int
{
return $this->getStartTimestamp($index, $date) + 60;
}
/**
* Convert a period to a real end time.
*
* @param int $timestamp The timestamp of the period.
*
* @return false|int False if the time is invalid, otherwise the real end time as a Unix timestamp.
*/
public function timestampToRealEnd(int $timestamp) : int
{
return $this->getRealTime($timestamp, $this->offsetGetByTimestamp($timestamp)->end);
}
/**
* Convert a period to a real start time.
*
* @param int $timestamp The timestamp of the period.
*
* @return false|int False if the time is invalid, otherwise the real start time as a Unix timestamp.
*/
public function timestampToRealStart(int $timestamp) : int
{
return $this->getRealTime($timestamp, $this->offsetGetByTimestamp($timestamp)->start);
}
/**
* Convert a period start or end time to a real time.
*
* @param int $timestamp The timestamp of the period
* @param string $hhmm The time in the format HH:MM
*/
private function getRealTime(int $timestamp, string $hhmm) : int
{
if (!isset($this->tzid))
{
throw new \Exception('No timezone set');
}
return timestamp_set_time($timestamp, $hhmm, $this->tzid);
}
/**
* Convert a period nominal start time in seconds (ie nominal seconds since
* midnight, ignoring DST transitions) to an index into the periods array.
*/
private static function nominalSecondsToIndex(int $seconds) : int
{
// Periods are counted as minutes from noon, ie 1200 is $period[0],
// 1201 $period[1], etc.
return intval($seconds/60) - (12*60);
}
/**
* Convert a timestamp to an index into the periods array.
*/
private function timestampToIndex(int $timestamp) : int
{
if (!isset($this->tzid))
{
throw new \Exception('No timezone set');
}
$noon = new DateTime('now', new DateTimeZone($this->tzid));
$noon->setTimestamp($timestamp);
$noon->setTime(12, 0);
return intval(($timestamp - $noon->getTimestamp())/60);
}
}