|
61 | 61 | use Sabre\VObject\Component; |
62 | 62 | use Sabre\VObject\Component\VCalendar; |
63 | 63 | use Sabre\VObject\Component\VTimeZone; |
64 | | -use Sabre\VObject\DateTimeParser; |
65 | 64 | use Sabre\VObject\InvalidDataException; |
66 | 65 | use Sabre\VObject\ParseException; |
67 | 66 | use Sabre\VObject\Property; |
68 | 67 | use Sabre\VObject\Reader; |
69 | | -use Sabre\VObject\Recur\EventIterator; |
70 | 68 | use Sabre\VObject\Recur\MaxInstancesExceededException; |
71 | 69 | use Sabre\VObject\Recur\NoInstancesException; |
72 | 70 | use function array_column; |
@@ -3065,99 +3063,83 @@ public function restoreChanges(int $calendarId, int $calendarType = self::CALEND |
3065 | 3063 | * @return array |
3066 | 3064 | */ |
3067 | 3065 | public function getDenormalizedData(string $calendarData): array { |
| 3066 | + |
| 3067 | + $derived = [ |
| 3068 | + 'etag' => md5($calendarData), |
| 3069 | + 'size' => strlen($calendarData), |
| 3070 | + ]; |
| 3071 | + // validate data and extract base component |
| 3072 | + /** @var VCalendar $vObject */ |
3068 | 3073 | $vObject = Reader::read($calendarData); |
3069 | | - $vEvents = []; |
3070 | | - $componentType = null; |
3071 | | - $component = null; |
3072 | | - $firstOccurrence = null; |
3073 | | - $lastOccurrence = null; |
3074 | | - $uid = null; |
3075 | | - $classification = self::CLASSIFICATION_PUBLIC; |
3076 | | - $hasDTSTART = false; |
3077 | | - foreach ($vObject->getComponents() as $component) { |
3078 | | - if ($component->name !== 'VTIMEZONE') { |
3079 | | - // Finding all VEVENTs, and track them |
3080 | | - if ($component->name === 'VEVENT') { |
3081 | | - $vEvents[] = $component; |
3082 | | - if ($component->DTSTART) { |
3083 | | - $hasDTSTART = true; |
| 3074 | + $components = $vObject->getBaseComponents(); |
| 3075 | + if (count($components) !== 1) { |
| 3076 | + throw new BadRequest('Invalid calendar object must contain exactly one VJOURNAL, VEVENT, or VTODO component type'); |
| 3077 | + } |
| 3078 | + $component = $components[0]; |
| 3079 | + // extract basic information |
| 3080 | + $derived['componentType'] = $component->name; |
| 3081 | + $derived['uid'] = $component->UID ? $component->UID->getValue() : null; |
| 3082 | + $derived['classification'] = $component->CLASS ? match ($component->CLASS->getValue()) { |
| 3083 | + 'PUBLIC' => self::CLASSIFICATION_PUBLIC, |
| 3084 | + 'CONFIDENTIAL' => self::CLASSIFICATION_CONFIDENTIAL, |
| 3085 | + default => self::CLASSIFICATION_PRIVATE, |
| 3086 | + } : self::CLASSIFICATION_PUBLIC; |
| 3087 | + // extract start and end dates |
| 3088 | + // VTODO components can have no start date |
| 3089 | + /** @var */ |
| 3090 | + $startDate = $component->DTSTART instanceof \Sabre\VObject\Property\ICalendar\DateTime ? $component->DTSTART->getDateTime() : null; |
| 3091 | + $endDate = $startDate ? clone $startDate : null; |
| 3092 | + if ($startDate) { |
| 3093 | + // Recurring |
| 3094 | + if ($component->RRULE || $component->RDATE) { |
| 3095 | + // RDATE can have both instances and multiple values |
| 3096 | + // RDATE;TZID=America/Toronto:20250701T000000,20260701T000000 |
| 3097 | + // RDATE;TZID=America/Toronto:20270701T000000 |
| 3098 | + if ($component->RDATE) { |
| 3099 | + foreach ($component->RDATE as $instance) { |
| 3100 | + foreach ($instance->getDateTimes() as $entry) { |
| 3101 | + if ($entry > $endDate) { |
| 3102 | + $endDate = $entry; |
| 3103 | + } |
| 3104 | + } |
3084 | 3105 | } |
3085 | 3106 | } |
3086 | | - // Track first component type and uid |
3087 | | - if ($uid === null) { |
3088 | | - $componentType = $component->name; |
3089 | | - $uid = (string)$component->UID; |
3090 | | - } |
3091 | | - } |
3092 | | - } |
3093 | | - if (!$componentType) { |
3094 | | - throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); |
3095 | | - } |
3096 | | - |
3097 | | - if ($hasDTSTART) { |
3098 | | - $component = $vEvents[0]; |
3099 | | - |
3100 | | - // Finding the last occurrence is a bit harder |
3101 | | - if (!isset($component->RRULE) && count($vEvents) === 1) { |
3102 | | - $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp(); |
3103 | | - if (isset($component->DTEND)) { |
3104 | | - $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp(); |
3105 | | - } elseif (isset($component->DURATION)) { |
3106 | | - $endDate = clone $component->DTSTART->getDateTime(); |
3107 | | - $endDate->add(DateTimeParser::parse($component->DURATION->getValue())); |
3108 | | - $lastOccurrence = $endDate->getTimeStamp(); |
3109 | | - } elseif (!$component->DTSTART->hasTime()) { |
3110 | | - $endDate = clone $component->DTSTART->getDateTime(); |
3111 | | - $endDate->modify('+1 day'); |
3112 | | - $lastOccurrence = $endDate->getTimeStamp(); |
3113 | | - } else { |
3114 | | - $lastOccurrence = $firstOccurrence; |
| 3107 | + // RRULE can be infinate or limited by a UNTIL or COUNT |
| 3108 | + if ($component->RRULE) { |
| 3109 | + try { |
| 3110 | + $rule = new EventReaderRRule($component->RRULE->getValue(), $startDate); |
| 3111 | + $endDate = $rule->isInfinite() ? new DateTime(self::MAX_DATE) : $rule->concludes(); |
| 3112 | + } catch (NoInstancesException $e) { |
| 3113 | + $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [ |
| 3114 | + 'app' => 'dav', |
| 3115 | + 'exception' => $e, |
| 3116 | + ]); |
| 3117 | + throw new Forbidden($e->getMessage()); |
| 3118 | + } |
3115 | 3119 | } |
| 3120 | + // Singleton |
3116 | 3121 | } else { |
3117 | | - try { |
3118 | | - $it = new EventIterator($vEvents); |
3119 | | - } catch (NoInstancesException $e) { |
3120 | | - $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [ |
3121 | | - 'app' => 'dav', |
3122 | | - 'exception' => $e, |
3123 | | - ]); |
3124 | | - throw new Forbidden($e->getMessage()); |
3125 | | - } |
3126 | | - $maxDate = new DateTime(self::MAX_DATE); |
3127 | | - $firstOccurrence = $it->getDtStart()->getTimestamp(); |
3128 | | - if ($it->isInfinite()) { |
3129 | | - $lastOccurrence = $maxDate->getTimestamp(); |
3130 | | - } else { |
3131 | | - $end = $it->getDtEnd(); |
3132 | | - while ($it->valid() && $end < $maxDate) { |
3133 | | - $end = $it->getDtEnd(); |
3134 | | - $it->next(); |
3135 | | - } |
3136 | | - $lastOccurrence = $end->getTimestamp(); |
| 3122 | + if ($component->DTEND instanceof \Sabre\VObject\Property\ICalendar\DateTime) { |
| 3123 | + // VEVENT component types |
| 3124 | + $endDate = $component->DTEND->getDateTime(); |
| 3125 | + } elseif ($component->DURATION instanceof \Sabre\VObject\Property\ICalendar\Duration) { |
| 3126 | + // VEVENT / VTODO component types |
| 3127 | + $endDate = $startDate->add($component->DURATION->getDateInterval()); |
| 3128 | + } elseif ($component->DUE instanceof \Sabre\VObject\Property\ICalendar\DateTime) { |
| 3129 | + // VTODO component types |
| 3130 | + $endDate = $component->DUE->getDateTime(); |
| 3131 | + } elseif ($component->name === 'VEVENT' && !$component->DTSTART->hasTime()) { |
| 3132 | + // VEVENT component type without time is automatically one day |
| 3133 | + $endDate = (clone $startDate)->modify('+1 day'); |
3137 | 3134 | } |
3138 | 3135 | } |
3139 | 3136 | } |
| 3137 | + // convert dates to timestamp and prevent negative values |
| 3138 | + $derived['firstOccurence'] = $startDate ? max(0, $startDate->getTimestamp()) : 0; |
| 3139 | + $derived['lastOccurence'] = $endDate ? max(0, $endDate->getTimestamp()) : 0; |
| 3140 | + |
| 3141 | + return $derived; |
3140 | 3142 |
|
3141 | | - if ($component->CLASS) { |
3142 | | - $classification = CalDavBackend::CLASSIFICATION_PRIVATE; |
3143 | | - switch ($component->CLASS->getValue()) { |
3144 | | - case 'PUBLIC': |
3145 | | - $classification = CalDavBackend::CLASSIFICATION_PUBLIC; |
3146 | | - break; |
3147 | | - case 'CONFIDENTIAL': |
3148 | | - $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL; |
3149 | | - break; |
3150 | | - } |
3151 | | - } |
3152 | | - return [ |
3153 | | - 'etag' => md5($calendarData), |
3154 | | - 'size' => strlen($calendarData), |
3155 | | - 'componentType' => $componentType, |
3156 | | - 'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence), |
3157 | | - 'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence), |
3158 | | - 'uid' => $uid, |
3159 | | - 'classification' => $classification |
3160 | | - ]; |
3161 | 3143 | } |
3162 | 3144 |
|
3163 | 3145 | /** |
|
0 commit comments