Skip to content

Commit 79c9ed4

Browse files
miaulalalaSebastianKrupinski
authored andcommitted
fix(caldav): improved data extraction for all component types
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
1 parent 0b354ef commit 79c9ed4

File tree

1 file changed

+68
-86
lines changed

1 file changed

+68
-86
lines changed

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 68 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,10 @@
6161
use Sabre\VObject\Component;
6262
use Sabre\VObject\Component\VCalendar;
6363
use Sabre\VObject\Component\VTimeZone;
64-
use Sabre\VObject\DateTimeParser;
6564
use Sabre\VObject\InvalidDataException;
6665
use Sabre\VObject\ParseException;
6766
use Sabre\VObject\Property;
6867
use Sabre\VObject\Reader;
69-
use Sabre\VObject\Recur\EventIterator;
7068
use Sabre\VObject\Recur\MaxInstancesExceededException;
7169
use Sabre\VObject\Recur\NoInstancesException;
7270
use function array_column;
@@ -3065,99 +3063,83 @@ public function restoreChanges(int $calendarId, int $calendarType = self::CALEND
30653063
* @return array
30663064
*/
30673065
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 */
30683073
$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+
}
30843105
}
30853106
}
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+
}
31153119
}
3120+
// Singleton
31163121
} 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');
31373134
}
31383135
}
31393136
}
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;
31403142

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-
];
31613143
}
31623144

31633145
/**

0 commit comments

Comments
 (0)