Docker image / push (push) Canceled after 0s
包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
239 lines
7.6 KiB
PHP
239 lines
7.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace MRBS\ICalendar;
|
|
|
|
use GuzzleHttp\Client;
|
|
use function MRBS\_tbl;
|
|
use function MRBS\db;
|
|
use function MRBS\row_cast_columns;
|
|
|
|
class Timezone extends Component
|
|
{
|
|
public const NAME = 'VTIMEZONE';
|
|
|
|
|
|
protected function validateProperty(Property $property): void
|
|
{
|
|
// TODO: Implement validateProperty() method.
|
|
}
|
|
|
|
|
|
/**
|
|
* Create an instance of the class based on a given timezone name.
|
|
*
|
|
* This method caches the latest VTIMEZONE component in the database. If it has expired,
|
|
* it goes to the web for the latest version, or, if there's nothing in the database in the
|
|
* first place, it tries to populate it from the VTIMEZONE definitions in the filesystem.
|
|
*
|
|
* @param string $tz The name of the timezone for which the instance should be created.
|
|
* @return self|false Returns an instance of the class representing the timezone,
|
|
* or false if the timezone could not be determined or is invalid.
|
|
*/
|
|
public static function createFromTimezoneName (string $tz)
|
|
{
|
|
global $zoneinfo_update, $zoneinfo_expiry, $zoneinfo_outlook_compatible;
|
|
|
|
static $vtimezones = array(); // Cache the components for performance
|
|
|
|
if (!isset($vtimezones[$tz]))
|
|
{
|
|
// Look for a timezone definition in the database
|
|
$vtimezone_db = self::getFromDb($tz, $zoneinfo_outlook_compatible);
|
|
if (isset($vtimezone_db['vtimezone']))
|
|
{
|
|
$vtimezones[$tz] = ComponentFactory::createFromString($vtimezone_db['vtimezone']);
|
|
// If the definition has expired, and we're updating it, then get a fresh definition from the URL
|
|
if ($zoneinfo_update && ((time() - $vtimezone_db['last_updated']) >= $zoneinfo_expiry))
|
|
{
|
|
$vtimezone = self::getFromUrl($vtimezone_db['vtimezone']);
|
|
if (isset($vtimezone))
|
|
{
|
|
// We've got a valid VTIMEZONE, so we can update the database and the static variable
|
|
self::upsertDb($tz, $zoneinfo_outlook_compatible, $vtimezone);
|
|
$vtimezones[$tz] = ComponentFactory::createFromString($vtimezone);
|
|
}
|
|
else
|
|
{
|
|
// If we didn't manage to get a new VTIMEZONE, update the last_updated field
|
|
// so that MRBS will not try again until after the expiry interval has passed.
|
|
// This will mean that we don't keep encountering a timeout delay. (The most
|
|
// likely reason that we couldn't get a new VTIMEZONE is that the site doesn't
|
|
// have external internet access, so there's no point in retrying for a while).
|
|
self::touchDb($tz, $zoneinfo_outlook_compatible);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If there's nothing in the database, get one from the filesystem
|
|
$vtimezone = self::getFromFile($tz, $zoneinfo_outlook_compatible);
|
|
if (isset($vtimezone))
|
|
{
|
|
// And put it in the database if it's valid
|
|
self::upsertDb($tz, $zoneinfo_outlook_compatible, $vtimezone);
|
|
$vtimezones[$tz] = ComponentFactory::createFromString($vtimezone);
|
|
}
|
|
else
|
|
{
|
|
// Everything has failed
|
|
$vtimezones[$tz] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $vtimezones[$tz];
|
|
}
|
|
|
|
|
|
/**
|
|
* Fetch timezone information from the database based on the given timezone identifier.
|
|
*
|
|
* @param string $tz The timezone identifier to retrieve data for.
|
|
* @return array{'vtimezone': string, 'last_updated': int} Returns an associative array containing the
|
|
* timezone information and last updated timestamp, or null if no matching record is found.
|
|
*/
|
|
private static function getFromDb(string $tz, bool $zoneinfo_outlook_compatible) : ?array
|
|
{
|
|
$sql = "SELECT vtimezone, last_updated
|
|
FROM " . _tbl('zoneinfo') . "
|
|
WHERE timezone=:timezone
|
|
AND outlook_compatible=:outlook_compatible
|
|
LIMIT 1";
|
|
|
|
$sql_params = array(
|
|
':timezone' => $tz,
|
|
':outlook_compatible' => ($zoneinfo_outlook_compatible) ? 1 : 0
|
|
);
|
|
|
|
$res = db()->query($sql, $sql_params);
|
|
|
|
if (false === ($row = $res->next_row_keyed()))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
row_cast_columns($row, 'zoneinfo');
|
|
|
|
return $row;
|
|
}
|
|
|
|
|
|
/**
|
|
* Fetch the VTIMEZONE component from a file for a given timezone identifier.
|
|
*/
|
|
private static function getFromFile(string $tz, bool $zoneinfo_outlook_compatible) : ?string
|
|
{
|
|
$tz_dir = ($zoneinfo_outlook_compatible) ? TZDIR_OUTLOOK : TZDIR;
|
|
$tz_file = "$tz_dir/$tz.ics";
|
|
|
|
if (!is_readable($tz_file))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$vcalendar = file_get_contents($tz_file);
|
|
|
|
if (empty($vcalendar))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$vtimezone = self::extractVtimezone($vcalendar);
|
|
|
|
return (empty($vtimezone)) ? null : $vtimezone;
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets a VTIMEZONE definition from the TZURL defined in the $vtimezone component string.
|
|
*/
|
|
private static function getFromUrl(string $vtimezone_string) : ?string
|
|
{
|
|
// (Note that a VTIMEZONE component can contain a TZURL property which
|
|
// gives the URL of the most up-to-date version. Calendar applications
|
|
// should be able to check this themselves, but we might as well give them
|
|
// the most up-to-date version in the first place).
|
|
if (false === ($vtimezone = ComponentFactory::createFromString($vtimezone_string)))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
$tz_url_values = $vtimezone->getPropertyValues('TZURL');
|
|
|
|
if (empty($tz_url_values))
|
|
{
|
|
trigger_error("The VTIMEZONE component didn't contain a TZURL property.", E_USER_NOTICE);
|
|
return null;
|
|
}
|
|
|
|
$tz_url = $tz_url_values[0];
|
|
|
|
try {
|
|
$vcalendar = (new Client())->get($tz_url)->getBody()->getContents();
|
|
}
|
|
catch (\Exception $e) {
|
|
trigger_error(get_class($e) . ': ' . $e->getMessage(), E_USER_WARNING);
|
|
trigger_error("MRBS: failed to download a new timezone definition from $tz_url", E_USER_WARNING);
|
|
return null;
|
|
}
|
|
|
|
$new_vtimezone = self::extractVtimezone($vcalendar);
|
|
if (empty($new_vtimezone))
|
|
{
|
|
trigger_error("MRBS: $tz_url did not contain a valid VTIMEZONE", E_USER_WARNING);
|
|
return null;
|
|
}
|
|
|
|
return $new_vtimezone;
|
|
}
|
|
|
|
|
|
/**
|
|
* Extract a VTIMEZONE component from a VCALENDAR.
|
|
*/
|
|
private static function extractVtimezone(string $content) : string
|
|
{
|
|
// The VTIMEZONE components are enclosed in a VCALENDAR, so we want to
|
|
// extract the VTIMEZONE component.
|
|
preg_match('/BEGIN:VTIMEZONE[\s\S]*?END:VTIMEZONE/', $content, $matches);
|
|
return $matches[0] ?? '';
|
|
}
|
|
|
|
|
|
// Update the last_updated time for a timezone in the database
|
|
private static function touchDb(string $tz, bool $zoneinfo_outlook_compatible) : void
|
|
{
|
|
$sql = "UPDATE " . _tbl('zoneinfo') . "
|
|
SET last_updated=:last_updated
|
|
WHERE timezone=:timezone
|
|
AND outlook_compatible=:outlook_compatible";
|
|
|
|
$sql_params = array(
|
|
':last_updated' => time(),
|
|
':timezone' => $tz,
|
|
':outlook_compatible' => ($zoneinfo_outlook_compatible) ? 1 : 0
|
|
);
|
|
|
|
db()->command($sql, $sql_params);
|
|
}
|
|
|
|
|
|
/**
|
|
* Inserts or updates a database record in the `zoneinfo` table with the VTIMEZONE data
|
|
* for a timezone.
|
|
*/
|
|
private static function upsertDb(string $tz, bool $zoneinfo_outlook_compatible, string $vtimezone) : void
|
|
{
|
|
$sql_params = [];
|
|
$data = [
|
|
'vtimezone' => $vtimezone,
|
|
'last_updated' => time(),
|
|
'timezone' => $tz,
|
|
'outlook_compatible' => ($zoneinfo_outlook_compatible) ? 1 : 0
|
|
];
|
|
$sql = db()->syntax_upsert($data, _tbl('zoneinfo'), $sql_params, ['timezone', 'outlook_compatible'], ['id'], true);
|
|
db()->command($sql, $sql_params);
|
|
}
|
|
|
|
}
|