|
59 | 59 | use Sabre\VObject\Component;
|
60 | 60 | use Sabre\VObject\Component\VCalendar;
|
61 | 61 | use Sabre\VObject\Component\VTimeZone;
|
62 |
| -use Sabre\VObject\DateTimeParser; |
63 | 62 | use Sabre\VObject\InvalidDataException;
|
64 | 63 | use Sabre\VObject\ParseException;
|
65 | 64 | use Sabre\VObject\Property;
|
66 | 65 | use Sabre\VObject\Reader;
|
67 |
| -use Sabre\VObject\Recur\EventIterator; |
68 | 66 | use Sabre\VObject\Recur\MaxInstancesExceededException;
|
69 | 67 | use Sabre\VObject\Recur\NoInstancesException;
|
70 | 68 | use function array_column;
|
@@ -2985,99 +2983,83 @@ public function restoreChanges(int $calendarId, int $calendarType = self::CALEND
|
2985 | 2983 | * @return array
|
2986 | 2984 | */
|
2987 | 2985 | public function getDenormalizedData(string $calendarData): array {
|
| 2986 | + |
| 2987 | + $derived = [ |
| 2988 | + 'etag' => md5($calendarData), |
| 2989 | + 'size' => strlen($calendarData), |
| 2990 | + ]; |
| 2991 | + // validate data and extract base component |
| 2992 | + /** @var VCalendar $vObject */ |
2988 | 2993 | $vObject = Reader::read($calendarData);
|
2989 |
| - $vEvents = []; |
2990 |
| - $componentType = null; |
2991 |
| - $component = null; |
2992 |
| - $firstOccurrence = null; |
2993 |
| - $lastOccurrence = null; |
2994 |
| - $uid = null; |
2995 |
| - $classification = self::CLASSIFICATION_PUBLIC; |
2996 |
| - $hasDTSTART = false; |
2997 |
| - foreach ($vObject->getComponents() as $component) { |
2998 |
| - if ($component->name !== 'VTIMEZONE') { |
2999 |
| - // Finding all VEVENTs, and track them |
3000 |
| - if ($component->name === 'VEVENT') { |
3001 |
| - $vEvents[] = $component; |
3002 |
| - if ($component->DTSTART) { |
3003 |
| - $hasDTSTART = true; |
| 2994 | + $components = $vObject->getBaseComponents(); |
| 2995 | + if (count($components) !== 1) { |
| 2996 | + throw new BadRequest('Invalid calendar object must contain exactly one VJOURNAL, VEVENT, or VTODO component type'); |
| 2997 | + } |
| 2998 | + $component = $components[0]; |
| 2999 | + // extract basic information |
| 3000 | + $derived['componentType'] = $component->name; |
| 3001 | + $derived['uid'] = $component->UID ? $component->UID->getValue() : null; |
| 3002 | + $derived['classification'] = $component->CLASS ? match ($component->CLASS->getValue()) { |
| 3003 | + 'PUBLIC' => self::CLASSIFICATION_PUBLIC, |
| 3004 | + 'CONFIDENTIAL' => self::CLASSIFICATION_CONFIDENTIAL, |
| 3005 | + default => self::CLASSIFICATION_PRIVATE, |
| 3006 | + } : self::CLASSIFICATION_PUBLIC; |
| 3007 | + // extract start and end dates |
| 3008 | + // VTODO components can have no start date |
| 3009 | + /** @var */ |
| 3010 | + $startDate = $component->DTSTART instanceof \Sabre\VObject\Property\ICalendar\DateTime ? $component->DTSTART->getDateTime() : null; |
| 3011 | + $endDate = $startDate ? clone $startDate : null; |
| 3012 | + if ($startDate) { |
| 3013 | + // Recurring |
| 3014 | + if ($component->RRULE || $component->RDATE) { |
| 3015 | + // RDATE can have both instances and multiple values |
| 3016 | + // RDATE;TZID=America/Toronto:20250701T000000,20260701T000000 |
| 3017 | + // RDATE;TZID=America/Toronto:20270701T000000 |
| 3018 | + if ($component->RDATE) { |
| 3019 | + foreach ($component->RDATE as $instance) { |
| 3020 | + foreach ($instance->getDateTimes() as $entry) { |
| 3021 | + if ($entry > $endDate) { |
| 3022 | + $endDate = $entry; |
| 3023 | + } |
| 3024 | + } |
3004 | 3025 | }
|
3005 | 3026 | }
|
3006 |
| - // Track first component type and uid |
3007 |
| - if ($uid === null) { |
3008 |
| - $componentType = $component->name; |
3009 |
| - $uid = (string)$component->UID; |
3010 |
| - } |
3011 |
| - } |
3012 |
| - } |
3013 |
| - if (!$componentType) { |
3014 |
| - throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); |
3015 |
| - } |
3016 |
| - |
3017 |
| - if ($hasDTSTART) { |
3018 |
| - $component = $vEvents[0]; |
3019 |
| - |
3020 |
| - // Finding the last occurrence is a bit harder |
3021 |
| - if (!isset($component->RRULE) && count($vEvents) === 1) { |
3022 |
| - $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp(); |
3023 |
| - if (isset($component->DTEND)) { |
3024 |
| - $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp(); |
3025 |
| - } elseif (isset($component->DURATION)) { |
3026 |
| - $endDate = clone $component->DTSTART->getDateTime(); |
3027 |
| - $endDate->add(DateTimeParser::parse($component->DURATION->getValue())); |
3028 |
| - $lastOccurrence = $endDate->getTimeStamp(); |
3029 |
| - } elseif (!$component->DTSTART->hasTime()) { |
3030 |
| - $endDate = clone $component->DTSTART->getDateTime(); |
3031 |
| - $endDate->modify('+1 day'); |
3032 |
| - $lastOccurrence = $endDate->getTimeStamp(); |
3033 |
| - } else { |
3034 |
| - $lastOccurrence = $firstOccurrence; |
| 3027 | + // RRULE can be infinate or limited by a UNTIL or COUNT |
| 3028 | + if ($component->RRULE) { |
| 3029 | + try { |
| 3030 | + $rule = new EventReaderRRule($component->RRULE->getValue(), $startDate); |
| 3031 | + $endDate = $rule->isInfinite() ? new DateTime(self::MAX_DATE) : $rule->concludes(); |
| 3032 | + } catch (NoInstancesException $e) { |
| 3033 | + $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [ |
| 3034 | + 'app' => 'dav', |
| 3035 | + 'exception' => $e, |
| 3036 | + ]); |
| 3037 | + throw new Forbidden($e->getMessage()); |
| 3038 | + } |
3035 | 3039 | }
|
| 3040 | + // Singleton |
3036 | 3041 | } else {
|
3037 |
| - try { |
3038 |
| - $it = new EventIterator($vEvents); |
3039 |
| - } catch (NoInstancesException $e) { |
3040 |
| - $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [ |
3041 |
| - 'app' => 'dav', |
3042 |
| - 'exception' => $e, |
3043 |
| - ]); |
3044 |
| - throw new Forbidden($e->getMessage()); |
3045 |
| - } |
3046 |
| - $maxDate = new DateTime(self::MAX_DATE); |
3047 |
| - $firstOccurrence = $it->getDtStart()->getTimestamp(); |
3048 |
| - if ($it->isInfinite()) { |
3049 |
| - $lastOccurrence = $maxDate->getTimestamp(); |
3050 |
| - } else { |
3051 |
| - $end = $it->getDtEnd(); |
3052 |
| - while ($it->valid() && $end < $maxDate) { |
3053 |
| - $end = $it->getDtEnd(); |
3054 |
| - $it->next(); |
3055 |
| - } |
3056 |
| - $lastOccurrence = $end->getTimestamp(); |
| 3042 | + if ($component->DTEND instanceof \Sabre\VObject\Property\ICalendar\DateTime) { |
| 3043 | + // VEVENT component types |
| 3044 | + $endDate = $component->DTEND->getDateTime(); |
| 3045 | + } elseif ($component->DURATION instanceof \Sabre\VObject\Property\ICalendar\Duration) { |
| 3046 | + // VEVENT / VTODO component types |
| 3047 | + $endDate = $startDate->add($component->DURATION->getDateInterval()); |
| 3048 | + } elseif ($component->DUE instanceof \Sabre\VObject\Property\ICalendar\DateTime) { |
| 3049 | + // VTODO component types |
| 3050 | + $endDate = $component->DUE->getDateTime(); |
| 3051 | + } elseif ($component->name === 'VEVENT' && !$component->DTSTART->hasTime()) { |
| 3052 | + // VEVENT component type without time is automatically one day |
| 3053 | + $endDate = (clone $startDate)->modify('+1 day'); |
3057 | 3054 | }
|
3058 | 3055 | }
|
3059 | 3056 | }
|
| 3057 | + // convert dates to timestamp and prevent negative values |
| 3058 | + $derived['firstOccurence'] = $startDate ? max(0, $startDate->getTimestamp()) : 0; |
| 3059 | + $derived['lastOccurence'] = $endDate ? max(0, $endDate->getTimestamp()) : 0; |
| 3060 | + |
| 3061 | + return $derived; |
3060 | 3062 |
|
3061 |
| - if ($component->CLASS) { |
3062 |
| - $classification = CalDavBackend::CLASSIFICATION_PRIVATE; |
3063 |
| - switch ($component->CLASS->getValue()) { |
3064 |
| - case 'PUBLIC': |
3065 |
| - $classification = CalDavBackend::CLASSIFICATION_PUBLIC; |
3066 |
| - break; |
3067 |
| - case 'CONFIDENTIAL': |
3068 |
| - $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL; |
3069 |
| - break; |
3070 |
| - } |
3071 |
| - } |
3072 |
| - return [ |
3073 |
| - 'etag' => md5($calendarData), |
3074 |
| - 'size' => strlen($calendarData), |
3075 |
| - 'componentType' => $componentType, |
3076 |
| - 'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence), |
3077 |
| - 'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence), |
3078 |
| - 'uid' => $uid, |
3079 |
| - 'classification' => $classification |
3080 |
| - ]; |
3081 | 3063 | }
|
3082 | 3064 |
|
3083 | 3065 | /**
|
|
0 commit comments