Skip to content

Commit 390d867

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

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+
/** @var \Sabre\VObject\Component\VEvent[]|\Sabre\VObject\Component\VTodo[]|\Sabre\VObject\Component\VJournal[] $components */
3427+
$components = $vObject->getBaseComponents();
3428+
if (count($components) !== 1) {
3429+
throw new BadRequest('A valid calendar object must contain at least one VJOURNAL, VEVENT, or VTODO component type');
3430+
}
3431+
$component = $components[0];
3432+
// extract basic information
3433+
$derived['componentType'] = $component->name;
3434+
$derived['uid'] = $component->UID ? $component->UID->getValue() : null;
3435+
$derived['classification'] = $component->CLASS ? match ($component->CLASS->getValue()) {
3436+
'PUBLIC' => self::CLASSIFICATION_PUBLIC,
3437+
'CONFIDENTIAL' => self::CLASSIFICATION_CONFIDENTIAL,
3438+
default => self::CLASSIFICATION_PRIVATE,
3439+
} : self::CLASSIFICATION_PUBLIC;
3440+
// extract start and end dates
3441+
// VTODO components can have no start date
3442+
/** @var */
3443+
$startDate = $component->DTSTART instanceof \Sabre\VObject\Property\ICalendar\DateTime ? $component->DTSTART->getDateTime() : null;
3444+
$endDate = $startDate ? clone $startDate : null;
3445+
if ($startDate) {
3446+
// Recurring
3447+
if ($component->RRULE || $component->RDATE) {
3448+
// RDATE can have both instances and multiple values
3449+
// RDATE;TZID=America/Toronto:20250701T000000,20260701T000000
3450+
// RDATE;TZID=America/Toronto:20270701T000000
3451+
if ($component->RDATE) {
3452+
foreach ($component->RDATE as $instance) {
3453+
foreach ($instance->getDateTimes() as $entry) {
3454+
if ($entry > $endDate) {
3455+
$endDate = $entry;
3456+
}
3457+
}
34363458
}
34373459
}
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;
3460+
// RRULE can be infinate or limited by a UNTIL or COUNT
3461+
if ($component->RRULE) {
3462+
try {
3463+
$rule = new EventReaderRRule($component->RRULE->getValue(), $startDate);
3464+
$endDate = $rule->isInfinite() ? new DateTime(self::MAX_DATE) : $rule->concludes();
3465+
} catch (NoInstancesException $e) {
3466+
$this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
3467+
'app' => 'dav',
3468+
'exception' => $e,
3469+
]);
3470+
throw new Forbidden($e->getMessage());
3471+
}
34673472
}
3473+
// Singleton
34683474
} 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();
3475+
if ($component->DTEND instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
3476+
// VEVENT component types
3477+
$endDate = $component->DTEND->getDateTime();
3478+
} elseif ($component->DURATION instanceof \Sabre\VObject\Property\ICalendar\Duration) {
3479+
// VEVENT / VTODO component types
3480+
$endDate = $startDate->add($component->DURATION->getDateInterval());
3481+
} elseif ($component->DUE instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
3482+
// VTODO component types
3483+
$endDate = $component->DUE->getDateTime();
3484+
} elseif ($component->name === 'VEVENT' && !$component->DTSTART->hasTime()) {
3485+
// VEVENT component type without time is automatically one day
3486+
$endDate = (clone $startDate)->modify('+1 day');
34893487
}
34903488
}
34913489
}
3490+
// convert dates to timestamp and prevent negative values
3491+
$derived['firstOccurence'] = $startDate ? max(0, $startDate->getTimestamp()) : 0;
3492+
$derived['lastOccurence'] = $endDate ? max(0, $endDate->getTimestamp()) : 0;
34923493

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-
];
3494+
return $derived;
35133495
}
35143496

35153497
/**

0 commit comments

Comments
 (0)