| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560 |
- <?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 (isset($matches['plusminus']) && '-' === $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;
- }
- }
|