mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-27 07:00:29 +01:00
561 lines
16 KiB
PHP
561 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Sabre\VObject;
|
|
|
|
use DateInterval;
|
|
use DateTimeImmutable;
|
|
use DateTimeZone;
|
|
|
|
/**
|
|
* DateTimeParser.
|
|
*
|
|
* This class is responsible for parsing the several different date and time
|
|
* formats iCalendar and vCards have.
|
|
*
|
|
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
|
|
* @author Evert Pot (http://evertpot.com/)
|
|
* @license http://sabre.io/license/ Modified BSD License
|
|
*/
|
|
class DateTimeParser
|
|
{
|
|
/**
|
|
* Parses an iCalendar (rfc5545) formatted datetime and returns a
|
|
* DateTimeImmutable object.
|
|
*
|
|
* Specifying a reference timezone is optional. It will only be used
|
|
* if the non-UTC format is used. The argument is used as a reference, the
|
|
* returned DateTimeImmutable object will still be in the UTC timezone.
|
|
*
|
|
* @param string $dt
|
|
* @param DateTimeZone $tz
|
|
*
|
|
* @return DateTimeImmutable
|
|
*/
|
|
public static function parseDateTime($dt, DateTimeZone $tz = null)
|
|
{
|
|
// Format is YYYYMMDD + "T" + hhmmss
|
|
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/', $dt, $matches);
|
|
|
|
if (!$result) {
|
|
throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
|
|
}
|
|
|
|
if ('Z' === $matches[7] || is_null($tz)) {
|
|
$tz = new DateTimeZone('UTC');
|
|
}
|
|
|
|
try {
|
|
$date = new DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3].' '.$matches[4].':'.$matches[5].':'.$matches[6], $tz);
|
|
} catch (\Exception $e) {
|
|
throw new InvalidDataException('The supplied iCalendar datetime value is incorrect: '.$dt);
|
|
}
|
|
|
|
return $date;
|
|
}
|
|
|
|
/**
|
|
* Parses an iCalendar (rfc5545) formatted date and returns a DateTimeImmutable object.
|
|
*
|
|
* @param string $date
|
|
* @param DateTimeZone $tz
|
|
*
|
|
* @return DateTimeImmutable
|
|
*/
|
|
public static function parseDate($date, DateTimeZone $tz = null)
|
|
{
|
|
// Format is YYYYMMDD
|
|
$result = preg_match('/^([0-9]{4})([0-1][0-9])([0-3][0-9])$/', $date, $matches);
|
|
|
|
if (!$result) {
|
|
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
|
|
}
|
|
|
|
if (is_null($tz)) {
|
|
$tz = new DateTimeZone('UTC');
|
|
}
|
|
|
|
try {
|
|
$date = new DateTimeImmutable($matches[1].'-'.$matches[2].'-'.$matches[3], $tz);
|
|
} catch (\Exception $e) {
|
|
throw new InvalidDataException('The supplied iCalendar date value is incorrect: '.$date);
|
|
}
|
|
|
|
return $date;
|
|
}
|
|
|
|
/**
|
|
* Parses an iCalendar (RFC5545) formatted duration value.
|
|
*
|
|
* This method will either return a DateTimeInterval object, or a string
|
|
* suitable for strtotime or DateTime::modify.
|
|
*
|
|
* @param string $duration
|
|
* @param bool $asString
|
|
*
|
|
* @return DateInterval|string
|
|
*/
|
|
public static function parseDuration($duration, $asString = false)
|
|
{
|
|
$result = preg_match('/^(?<plusminus>\+|-)?P((?<week>\d+)W)?((?<day>\d+)D)?(T((?<hour>\d+)H)?((?<minute>\d+)M)?((?<second>\d+)S)?)?$/', $duration, $matches);
|
|
if (!$result) {
|
|
throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration);
|
|
}
|
|
|
|
if (!$asString) {
|
|
$invert = false;
|
|
|
|
if ('-' === $matches['plusminus']) {
|
|
$invert = true;
|
|
}
|
|
|
|
$parts = [
|
|
'week',
|
|
'day',
|
|
'hour',
|
|
'minute',
|
|
'second',
|
|
];
|
|
|
|
foreach ($parts as $part) {
|
|
$matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0;
|
|
}
|
|
|
|
// We need to re-construct the $duration string, because weeks and
|
|
// days are not supported by DateInterval in the same string.
|
|
$duration = 'P';
|
|
$days = $matches['day'];
|
|
|
|
if ($matches['week']) {
|
|
$days += $matches['week'] * 7;
|
|
}
|
|
|
|
if ($days) {
|
|
$duration .= $days.'D';
|
|
}
|
|
|
|
if ($matches['minute'] || $matches['second'] || $matches['hour']) {
|
|
$duration .= 'T';
|
|
|
|
if ($matches['hour']) {
|
|
$duration .= $matches['hour'].'H';
|
|
}
|
|
|
|
if ($matches['minute']) {
|
|
$duration .= $matches['minute'].'M';
|
|
}
|
|
|
|
if ($matches['second']) {
|
|
$duration .= $matches['second'].'S';
|
|
}
|
|
}
|
|
|
|
if ('P' === $duration) {
|
|
$duration = 'PT0S';
|
|
}
|
|
|
|
$iv = new DateInterval($duration);
|
|
|
|
if ($invert) {
|
|
$iv->invert = true;
|
|
}
|
|
|
|
return $iv;
|
|
}
|
|
|
|
$parts = [
|
|
'week',
|
|
'day',
|
|
'hour',
|
|
'minute',
|
|
'second',
|
|
];
|
|
|
|
$newDur = '';
|
|
|
|
foreach ($parts as $part) {
|
|
if (isset($matches[$part]) && $matches[$part]) {
|
|
$newDur .= ' '.$matches[$part].' '.$part.'s';
|
|
}
|
|
}
|
|
|
|
$newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur);
|
|
|
|
if ('+' === $newDur) {
|
|
$newDur = '+0 seconds';
|
|
}
|
|
|
|
return $newDur;
|
|
}
|
|
|
|
/**
|
|
* Parses either a Date or DateTime, or Duration value.
|
|
*
|
|
* @param string $date
|
|
* @param DateTimeZone|string $referenceTz
|
|
*
|
|
* @return DateTimeImmutable|DateInterval
|
|
*/
|
|
public static function parse($date, $referenceTz = null)
|
|
{
|
|
if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) {
|
|
return self::parseDuration($date);
|
|
} elseif (8 === strlen($date)) {
|
|
return self::parseDate($date, $referenceTz);
|
|
} else {
|
|
return self::parseDateTime($date, $referenceTz);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method parses a vCard date and or time value.
|
|
*
|
|
* This can be used for the DATE, DATE-TIME, TIMESTAMP and
|
|
* DATE-AND-OR-TIME value.
|
|
*
|
|
* This method returns an array, not a DateTime value.
|
|
*
|
|
* The elements in the array are in the following order:
|
|
* year, month, date, hour, minute, second, timezone
|
|
*
|
|
* Almost any part of the string may be omitted. It's for example legal to
|
|
* just specify seconds, leave out the year, etc.
|
|
*
|
|
* Timezone is either returned as 'Z' or as '+0800'
|
|
*
|
|
* For any non-specified values null is returned.
|
|
*
|
|
* List of date formats that are supported:
|
|
* YYYY
|
|
* YYYY-MM
|
|
* YYYYMMDD
|
|
* --MMDD
|
|
* ---DD
|
|
*
|
|
* YYYY-MM-DD
|
|
* --MM-DD
|
|
* ---DD
|
|
*
|
|
* List of supported time formats:
|
|
*
|
|
* HH
|
|
* HHMM
|
|
* HHMMSS
|
|
* -MMSS
|
|
* --SS
|
|
*
|
|
* HH
|
|
* HH:MM
|
|
* HH:MM:SS
|
|
* -MM:SS
|
|
* --SS
|
|
*
|
|
* A full basic-format date-time string looks like :
|
|
* 20130603T133901
|
|
*
|
|
* A full extended-format date-time string looks like :
|
|
* 2013-06-03T13:39:01
|
|
*
|
|
* Times may be postfixed by a timezone offset. This can be either 'Z' for
|
|
* UTC, or a string like -0500 or +1100.
|
|
*
|
|
* @param string $date
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function parseVCardDateTime($date)
|
|
{
|
|
$regex = '/^
|
|
(?: # date part
|
|
(?:
|
|
(?: (?<year> [0-9]{4}) (?: -)?| --)
|
|
(?<month> [0-9]{2})?
|
|
|---)
|
|
(?<date> [0-9]{2})?
|
|
)?
|
|
(?:T # time part
|
|
(?<hour> [0-9]{2} | -)
|
|
(?<minute> [0-9]{2} | -)?
|
|
(?<second> [0-9]{2})?
|
|
|
|
(?: \.[0-9]{3})? # milliseconds
|
|
(?P<timezone> # timezone offset
|
|
|
|
Z | (?: \+|-)(?: [0-9]{4})
|
|
|
|
)?
|
|
|
|
)?
|
|
$/x';
|
|
|
|
if (!preg_match($regex, $date, $matches)) {
|
|
// Attempting to parse the extended format.
|
|
$regex = '/^
|
|
(?: # date part
|
|
(?: (?<year> [0-9]{4}) - | -- )
|
|
(?<month> [0-9]{2}) -
|
|
(?<date> [0-9]{2})
|
|
)?
|
|
(?:T # time part
|
|
|
|
(?: (?<hour> [0-9]{2}) : | -)
|
|
(?: (?<minute> [0-9]{2}) : | -)?
|
|
(?<second> [0-9]{2})?
|
|
|
|
(?: \.[0-9]{3})? # milliseconds
|
|
(?P<timezone> # timezone offset
|
|
|
|
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
|
|
|
|
)?
|
|
|
|
)?
|
|
$/x';
|
|
|
|
if (!preg_match($regex, $date, $matches)) {
|
|
throw new InvalidDataException('Invalid vCard date-time string: '.$date);
|
|
}
|
|
}
|
|
$parts = [
|
|
'year',
|
|
'month',
|
|
'date',
|
|
'hour',
|
|
'minute',
|
|
'second',
|
|
'timezone',
|
|
];
|
|
|
|
$result = [];
|
|
foreach ($parts as $part) {
|
|
if (empty($matches[$part])) {
|
|
$result[$part] = null;
|
|
} elseif ('-' === $matches[$part] || '--' === $matches[$part]) {
|
|
$result[$part] = null;
|
|
} else {
|
|
$result[$part] = $matches[$part];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* This method parses a vCard TIME value.
|
|
*
|
|
* This method returns an array, not a DateTime value.
|
|
*
|
|
* The elements in the array are in the following order:
|
|
* hour, minute, second, timezone
|
|
*
|
|
* Almost any part of the string may be omitted. It's for example legal to
|
|
* just specify seconds, leave out the hour etc.
|
|
*
|
|
* Timezone is either returned as 'Z' or as '+08:00'
|
|
*
|
|
* For any non-specified values null is returned.
|
|
*
|
|
* List of supported time formats:
|
|
*
|
|
* HH
|
|
* HHMM
|
|
* HHMMSS
|
|
* -MMSS
|
|
* --SS
|
|
*
|
|
* HH
|
|
* HH:MM
|
|
* HH:MM:SS
|
|
* -MM:SS
|
|
* --SS
|
|
*
|
|
* A full basic-format time string looks like :
|
|
* 133901
|
|
*
|
|
* A full extended-format time string looks like :
|
|
* 13:39:01
|
|
*
|
|
* Times may be postfixed by a timezone offset. This can be either 'Z' for
|
|
* UTC, or a string like -0500 or +11:00.
|
|
*
|
|
* @param string $date
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function parseVCardTime($date)
|
|
{
|
|
$regex = '/^
|
|
(?<hour> [0-9]{2} | -)
|
|
(?<minute> [0-9]{2} | -)?
|
|
(?<second> [0-9]{2})?
|
|
|
|
(?: \.[0-9]{3})? # milliseconds
|
|
(?P<timezone> # timezone offset
|
|
|
|
Z | (?: \+|-)(?: [0-9]{4})
|
|
|
|
)?
|
|
$/x';
|
|
|
|
if (!preg_match($regex, $date, $matches)) {
|
|
// Attempting to parse the extended format.
|
|
$regex = '/^
|
|
(?: (?<hour> [0-9]{2}) : | -)
|
|
(?: (?<minute> [0-9]{2}) : | -)?
|
|
(?<second> [0-9]{2})?
|
|
|
|
(?: \.[0-9]{3})? # milliseconds
|
|
(?P<timezone> # timezone offset
|
|
|
|
Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
|
|
|
|
)?
|
|
$/x';
|
|
|
|
if (!preg_match($regex, $date, $matches)) {
|
|
throw new InvalidDataException('Invalid vCard time string: '.$date);
|
|
}
|
|
}
|
|
$parts = [
|
|
'hour',
|
|
'minute',
|
|
'second',
|
|
'timezone',
|
|
];
|
|
|
|
$result = [];
|
|
foreach ($parts as $part) {
|
|
if (empty($matches[$part])) {
|
|
$result[$part] = null;
|
|
} elseif ('-' === $matches[$part]) {
|
|
$result[$part] = null;
|
|
} else {
|
|
$result[$part] = $matches[$part];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* This method parses a vCard date and or time value.
|
|
*
|
|
* This can be used for the DATE, DATE-TIME and
|
|
* DATE-AND-OR-TIME value.
|
|
*
|
|
* This method returns an array, not a DateTime value.
|
|
* The elements in the array are in the following order:
|
|
* year, month, date, hour, minute, second, timezone
|
|
* Almost any part of the string may be omitted. It's for example legal to
|
|
* just specify seconds, leave out the year, etc.
|
|
*
|
|
* Timezone is either returned as 'Z' or as '+0800'
|
|
*
|
|
* For any non-specified values null is returned.
|
|
*
|
|
* List of date formats that are supported:
|
|
* 20150128
|
|
* 2015-01
|
|
* --01
|
|
* --0128
|
|
* ---28
|
|
*
|
|
* List of supported time formats:
|
|
* 13
|
|
* 1353
|
|
* 135301
|
|
* -53
|
|
* -5301
|
|
* --01 (unreachable, see the tests)
|
|
* --01Z
|
|
* --01+1234
|
|
*
|
|
* List of supported date-time formats:
|
|
* 20150128T13
|
|
* --0128T13
|
|
* ---28T13
|
|
* ---28T1353
|
|
* ---28T135301
|
|
* ---28T13Z
|
|
* ---28T13+1234
|
|
*
|
|
* See the regular expressions for all the possible patterns.
|
|
*
|
|
* Times may be postfixed by a timezone offset. This can be either 'Z' for
|
|
* UTC, or a string like -0500 or +1100.
|
|
*
|
|
* @param string $date
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function parseVCardDateAndOrTime($date)
|
|
{
|
|
// \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d
|
|
$valueDate = '/^(?J)(?:'.
|
|
'(?<year>\d{4})(?<month>\d\d)(?<date>\d\d)'.
|
|
'|(?<year>\d{4})-(?<month>\d\d)'.
|
|
'|--(?<month>\d\d)(?<date>\d\d)?'.
|
|
'|---(?<date>\d\d)'.
|
|
')$/';
|
|
|
|
// (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)?
|
|
$valueTime = '/^(?J)(?:'.
|
|
'((?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?'.
|
|
'|-(?<minute>\d\d)(?<second>\d\d)?'.
|
|
'|--(?<second>\d\d))'.
|
|
'(?<timezone>(Z|[+\-]\d\d(\d\d)?))?'.
|
|
')$/';
|
|
|
|
// (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)?
|
|
$valueDateTime = '/^(?:'.
|
|
'((?<year0>\d{4})(?<month0>\d\d)(?<date0>\d\d)'.
|
|
'|--(?<month1>\d\d)(?<date1>\d\d)'.
|
|
'|---(?<date2>\d\d))'.
|
|
'T'.
|
|
'(?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?'.
|
|
'(?<timezone>(Z|[+\-]\d\d(\d\d?)))?'.
|
|
')$/';
|
|
|
|
// date-and-or-time is date | date-time | time
|
|
// in this strict order.
|
|
|
|
if (0 === preg_match($valueDate, $date, $matches)
|
|
&& 0 === preg_match($valueDateTime, $date, $matches)
|
|
&& 0 === preg_match($valueTime, $date, $matches)) {
|
|
throw new InvalidDataException('Invalid vCard date-time string: '.$date);
|
|
}
|
|
|
|
$parts = [
|
|
'year' => null,
|
|
'month' => null,
|
|
'date' => null,
|
|
'hour' => null,
|
|
'minute' => null,
|
|
'second' => null,
|
|
'timezone' => null,
|
|
];
|
|
|
|
// The $valueDateTime expression has a bug with (?J) so we simulate it.
|
|
$parts['date0'] = &$parts['date'];
|
|
$parts['date1'] = &$parts['date'];
|
|
$parts['date2'] = &$parts['date'];
|
|
$parts['month0'] = &$parts['month'];
|
|
$parts['month1'] = &$parts['month'];
|
|
$parts['year0'] = &$parts['year'];
|
|
|
|
foreach ($parts as $part => &$value) {
|
|
if (!empty($matches[$part])) {
|
|
$value = $matches[$part];
|
|
}
|
|
}
|
|
|
|
unset($parts['date0']);
|
|
unset($parts['date1']);
|
|
unset($parts['date2']);
|
|
unset($parts['month0']);
|
|
unset($parts['month1']);
|
|
unset($parts['year0']);
|
|
|
|
return $parts;
|
|
}
|
|
}
|