Skip to content

Commit 06fbea0

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

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

3158-
if ($component->CLASS) {
3159-
$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
3160-
switch ($component->CLASS->getValue()) {
3161-
case 'PUBLIC':
3162-
$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
3163-
break;
3164-
case 'CONFIDENTIAL':
3165-
$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
3166-
break;
3167-
}
3168-
}
3169-
return [
3170-
'etag' => md5($calendarData),
3171-
'size' => strlen($calendarData),
3172-
'componentType' => $componentType,
3173-
'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
3174-
'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
3175-
'uid' => $uid,
3176-
'classification' => $classification
3177-
];
31783160
}
31793161

31803162
/**

0 commit comments

Comments
 (0)