Skip to content

Commit 4171caf

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

1 file changed

Lines changed: 68 additions & 86 deletions

File tree

apps/dav/lib/CalDAV/CalDavBackend.php

Lines changed: 68 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,10 @@
6868
use Sabre\VObject\Component;
6969
use Sabre\VObject\Component\VCalendar;
7070
use Sabre\VObject\Component\VTimeZone;
71-
use Sabre\VObject\DateTimeParser;
7271
use Sabre\VObject\InvalidDataException;
7372
use Sabre\VObject\ParseException;
7473
use Sabre\VObject\Property;
7574
use Sabre\VObject\Reader;
76-
use Sabre\VObject\Recur\EventIterator;
7775
use Sabre\VObject\Recur\MaxInstancesExceededException;
7876
use Sabre\VObject\Recur\NoInstancesException;
7977
use function array_column;
@@ -3417,99 +3415,83 @@ public function restoreChanges(int $calendarId, int $calendarType = self::CALEND
34173415
* @return array
34183416
*/
34193417
public function getDenormalizedData(string $calendarData): array {
3418+
3419+
$derived = [
3420+
'etag' => md5($calendarData),
3421+
'size' => strlen($calendarData),
3422+
];
3423+
// validate data and extract base component
3424+
/** @var VCalendar $vObject */
34203425
$vObject = Reader::read($calendarData);
3421-
$vEvents = [];
3422-
$componentType = null;
3423-
$component = null;
3424-
$firstOccurrence = null;
3425-
$lastOccurrence = null;
3426-
$uid = null;
3427-
$classification = self::CLASSIFICATION_PUBLIC;
3428-
$hasDTSTART = false;
3429-
foreach ($vObject->getComponents() as $component) {
3430-
if ($component->name !== 'VTIMEZONE') {
3431-
// Finding all VEVENTs, and track them
3432-
if ($component->name === 'VEVENT') {
3433-
$vEvents[] = $component;
3434-
if ($component->DTSTART) {
3435-
$hasDTSTART = true;
3426+
$components = $vObject->getBaseComponents();
3427+
if (count($components) !== 1) {
3428+
throw new BadRequest('Invalid calendar object must contain exactly one VJOURNAL, VEVENT, or VTODO component type');
3429+
}
3430+
$component = $components[0];
3431+
// extract basic information
3432+
$derived['componentType'] = $component->name;
3433+
$derived['uid'] = $component->UID ? $component->UID->getValue() : null;
3434+
$derived['classification'] = $component->CLASS ? match ($component->CLASS->getValue()) {
3435+
'PUBLIC' => self::CLASSIFICATION_PUBLIC,
3436+
'CONFIDENTIAL' => self::CLASSIFICATION_CONFIDENTIAL,
3437+
default => self::CLASSIFICATION_PRIVATE,
3438+
} : self::CLASSIFICATION_PUBLIC;
3439+
// extract start and end dates
3440+
// VTODO components can have no start date
3441+
/** @var */
3442+
$startDate = $component->DTSTART instanceof \Sabre\VObject\Property\ICalendar\DateTime ? $component->DTSTART->getDateTime() : null;
3443+
$endDate = $startDate ? clone $startDate : null;
3444+
if ($startDate) {
3445+
// Recurring
3446+
if ($component->RRULE || $component->RDATE) {
3447+
// RDATE can have both instances and multiple values
3448+
// RDATE;TZID=America/Toronto:20250701T000000,20260701T000000
3449+
// RDATE;TZID=America/Toronto:20270701T000000
3450+
if ($component->RDATE) {
3451+
foreach ($component->RDATE as $instance) {
3452+
foreach ($instance->getDateTimes() as $entry) {
3453+
if ($entry > $endDate) {
3454+
$endDate = $entry;
3455+
}
3456+
}
34363457
}
34373458
}
3438-
// Track first component type and uid
3439-
if ($uid === null) {
3440-
$componentType = $component->name;
3441-
$uid = (string)$component->UID;
3442-
}
3443-
}
3444-
}
3445-
if (!$componentType) {
3446-
throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
3447-
}
3448-
3449-
if ($hasDTSTART) {
3450-
$component = $vEvents[0];
3451-
3452-
// Finding the last occurrence is a bit harder
3453-
if (!isset($component->RRULE) && count($vEvents) === 1) {
3454-
$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
3455-
if (isset($component->DTEND)) {
3456-
$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
3457-
} elseif (isset($component->DURATION)) {
3458-
$endDate = clone $component->DTSTART->getDateTime();
3459-
$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
3460-
$lastOccurrence = $endDate->getTimeStamp();
3461-
} elseif (!$component->DTSTART->hasTime()) {
3462-
$endDate = clone $component->DTSTART->getDateTime();
3463-
$endDate->modify('+1 day');
3464-
$lastOccurrence = $endDate->getTimeStamp();
3465-
} else {
3466-
$lastOccurrence = $firstOccurrence;
3459+
// RRULE can be infinate or limited by a UNTIL or COUNT
3460+
if ($component->RRULE) {
3461+
try {
3462+
$rule = new EventReaderRRule($component->RRULE->getValue(), $startDate);
3463+
$endDate = $rule->isInfinite() ? new DateTime(self::MAX_DATE) : $rule->concludes();
3464+
} catch (NoInstancesException $e) {
3465+
$this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
3466+
'app' => 'dav',
3467+
'exception' => $e,
3468+
]);
3469+
throw new Forbidden($e->getMessage());
3470+
}
34673471
}
3472+
// Singleton
34683473
} else {
3469-
try {
3470-
$it = new EventIterator($vEvents);
3471-
} catch (NoInstancesException $e) {
3472-
$this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
3473-
'app' => 'dav',
3474-
'exception' => $e,
3475-
]);
3476-
throw new Forbidden($e->getMessage());
3477-
}
3478-
$maxDate = new DateTime(self::MAX_DATE);
3479-
$firstOccurrence = $it->getDtStart()->getTimestamp();
3480-
if ($it->isInfinite()) {
3481-
$lastOccurrence = $maxDate->getTimestamp();
3482-
} else {
3483-
$end = $it->getDtEnd();
3484-
while ($it->valid() && $end < $maxDate) {
3485-
$end = $it->getDtEnd();
3486-
$it->next();
3487-
}
3488-
$lastOccurrence = $end->getTimestamp();
3474+
if ($component->DTEND instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
3475+
// VEVENT component types
3476+
$endDate = $component->DTEND->getDateTime();
3477+
} elseif ($component->DURATION instanceof \Sabre\VObject\Property\ICalendar\Duration) {
3478+
// VEVENT / VTODO component types
3479+
$endDate = $startDate->add($component->DURATION->getDateInterval());
3480+
} elseif ($component->DUE instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
3481+
// VTODO component types
3482+
$endDate = $component->DUE->getDateTime();
3483+
} elseif ($component->name === 'VEVENT' && !$component->DTSTART->hasTime()) {
3484+
// VEVENT component type without time is automatically one day
3485+
$endDate = (clone $startDate)->modify('+1 day');
34893486
}
34903487
}
34913488
}
3489+
// convert dates to timestamp and prevent negative values
3490+
$derived['firstOccurence'] = $startDate ? max(0, $startDate->getTimestamp()) : 0;
3491+
$derived['lastOccurence'] = $endDate ? max(0, $endDate->getTimestamp()) : 0;
3492+
3493+
return $derived;
34923494

3493-
if ($component->CLASS) {
3494-
$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
3495-
switch ($component->CLASS->getValue()) {
3496-
case 'PUBLIC':
3497-
$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
3498-
break;
3499-
case 'CONFIDENTIAL':
3500-
$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
3501-
break;
3502-
}
3503-
}
3504-
return [
3505-
'etag' => md5($calendarData),
3506-
'size' => strlen($calendarData),
3507-
'componentType' => $componentType,
3508-
'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
3509-
'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
3510-
'uid' => $uid,
3511-
'classification' => $classification
3512-
];
35133495
}
35143496

35153497
/**

0 commit comments

Comments
 (0)