Skip to content

Commit 548131b

Browse files
miaulalalaSebastianKrupinski
authored andcommitted
fix(caldav): improved data extraction for all component types
Signed-off-by: SebastianKrupinski <[email protected]>
1 parent c3f19da commit 548131b

File tree

1 file changed

+68
-86
lines changed

1 file changed

+68
-86
lines changed

Diff for: apps/dav/lib/CalDAV/CalDavBackend.php

+68-86
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,10 @@
5959
use Sabre\VObject\Component;
6060
use Sabre\VObject\Component\VCalendar;
6161
use Sabre\VObject\Component\VTimeZone;
62-
use Sabre\VObject\DateTimeParser;
6362
use Sabre\VObject\InvalidDataException;
6463
use Sabre\VObject\ParseException;
6564
use Sabre\VObject\Property;
6665
use Sabre\VObject\Reader;
67-
use Sabre\VObject\Recur\EventIterator;
6866
use Sabre\VObject\Recur\MaxInstancesExceededException;
6967
use Sabre\VObject\Recur\NoInstancesException;
7068
use function array_column;
@@ -2985,99 +2983,83 @@ public function restoreChanges(int $calendarId, int $calendarType = self::CALEND
29852983
* @return array
29862984
*/
29872985
public function getDenormalizedData(string $calendarData): array {
2986+
2987+
$derived = [
2988+
'etag' => md5($calendarData),
2989+
'size' => strlen($calendarData),
2990+
];
2991+
// validate data and extract base component
2992+
/** @var VCalendar $vObject */
29882993
$vObject = Reader::read($calendarData);
2989-
$vEvents = [];
2990-
$componentType = null;
2991-
$component = null;
2992-
$firstOccurrence = null;
2993-
$lastOccurrence = null;
2994-
$uid = null;
2995-
$classification = self::CLASSIFICATION_PUBLIC;
2996-
$hasDTSTART = false;
2997-
foreach ($vObject->getComponents() as $component) {
2998-
if ($component->name !== 'VTIMEZONE') {
2999-
// Finding all VEVENTs, and track them
3000-
if ($component->name === 'VEVENT') {
3001-
$vEvents[] = $component;
3002-
if ($component->DTSTART) {
3003-
$hasDTSTART = true;
2994+
$components = $vObject->getBaseComponents();
2995+
if (count($components) !== 1) {
2996+
throw new BadRequest('Invalid calendar object must contain exactly one VJOURNAL, VEVENT, or VTODO component type');
2997+
}
2998+
$component = $components[0];
2999+
// extract basic information
3000+
$derived['componentType'] = $component->name;
3001+
$derived['uid'] = $component->UID ? $component->UID->getValue() : null;
3002+
$derived['classification'] = $component->CLASS ? match ($component->CLASS->getValue()) {
3003+
'PUBLIC' => self::CLASSIFICATION_PUBLIC,
3004+
'CONFIDENTIAL' => self::CLASSIFICATION_CONFIDENTIAL,
3005+
default => self::CLASSIFICATION_PRIVATE,
3006+
} : self::CLASSIFICATION_PUBLIC;
3007+
// extract start and end dates
3008+
// VTODO components can have no start date
3009+
/** @var */
3010+
$startDate = $component->DTSTART instanceof \Sabre\VObject\Property\ICalendar\DateTime ? $component->DTSTART->getDateTime() : null;
3011+
$endDate = $startDate ? clone $startDate : null;
3012+
if ($startDate) {
3013+
// Recurring
3014+
if ($component->RRULE || $component->RDATE) {
3015+
// RDATE can have both instances and multiple values
3016+
// RDATE;TZID=America/Toronto:20250701T000000,20260701T000000
3017+
// RDATE;TZID=America/Toronto:20270701T000000
3018+
if ($component->RDATE) {
3019+
foreach ($component->RDATE as $instance) {
3020+
foreach ($instance->getDateTimes() as $entry) {
3021+
if ($entry > $endDate) {
3022+
$endDate = $entry;
3023+
}
3024+
}
30043025
}
30053026
}
3006-
// Track first component type and uid
3007-
if ($uid === null) {
3008-
$componentType = $component->name;
3009-
$uid = (string)$component->UID;
3010-
}
3011-
}
3012-
}
3013-
if (!$componentType) {
3014-
throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
3015-
}
3016-
3017-
if ($hasDTSTART) {
3018-
$component = $vEvents[0];
3019-
3020-
// Finding the last occurrence is a bit harder
3021-
if (!isset($component->RRULE) && count($vEvents) === 1) {
3022-
$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
3023-
if (isset($component->DTEND)) {
3024-
$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
3025-
} elseif (isset($component->DURATION)) {
3026-
$endDate = clone $component->DTSTART->getDateTime();
3027-
$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
3028-
$lastOccurrence = $endDate->getTimeStamp();
3029-
} elseif (!$component->DTSTART->hasTime()) {
3030-
$endDate = clone $component->DTSTART->getDateTime();
3031-
$endDate->modify('+1 day');
3032-
$lastOccurrence = $endDate->getTimeStamp();
3033-
} else {
3034-
$lastOccurrence = $firstOccurrence;
3027+
// RRULE can be infinate or limited by a UNTIL or COUNT
3028+
if ($component->RRULE) {
3029+
try {
3030+
$rule = new EventReaderRRule($component->RRULE->getValue(), $startDate);
3031+
$endDate = $rule->isInfinite() ? new DateTime(self::MAX_DATE) : $rule->concludes();
3032+
} catch (NoInstancesException $e) {
3033+
$this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
3034+
'app' => 'dav',
3035+
'exception' => $e,
3036+
]);
3037+
throw new Forbidden($e->getMessage());
3038+
}
30353039
}
3040+
// Singleton
30363041
} else {
3037-
try {
3038-
$it = new EventIterator($vEvents);
3039-
} catch (NoInstancesException $e) {
3040-
$this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
3041-
'app' => 'dav',
3042-
'exception' => $e,
3043-
]);
3044-
throw new Forbidden($e->getMessage());
3045-
}
3046-
$maxDate = new DateTime(self::MAX_DATE);
3047-
$firstOccurrence = $it->getDtStart()->getTimestamp();
3048-
if ($it->isInfinite()) {
3049-
$lastOccurrence = $maxDate->getTimestamp();
3050-
} else {
3051-
$end = $it->getDtEnd();
3052-
while ($it->valid() && $end < $maxDate) {
3053-
$end = $it->getDtEnd();
3054-
$it->next();
3055-
}
3056-
$lastOccurrence = $end->getTimestamp();
3042+
if ($component->DTEND instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
3043+
// VEVENT component types
3044+
$endDate = $component->DTEND->getDateTime();
3045+
} elseif ($component->DURATION instanceof \Sabre\VObject\Property\ICalendar\Duration) {
3046+
// VEVENT / VTODO component types
3047+
$endDate = $startDate->add($component->DURATION->getDateInterval());
3048+
} elseif ($component->DUE instanceof \Sabre\VObject\Property\ICalendar\DateTime) {
3049+
// VTODO component types
3050+
$endDate = $component->DUE->getDateTime();
3051+
} elseif ($component->name === 'VEVENT' && !$component->DTSTART->hasTime()) {
3052+
// VEVENT component type without time is automatically one day
3053+
$endDate = (clone $startDate)->modify('+1 day');
30573054
}
30583055
}
30593056
}
3057+
// convert dates to timestamp and prevent negative values
3058+
$derived['firstOccurence'] = $startDate ? max(0, $startDate->getTimestamp()) : 0;
3059+
$derived['lastOccurence'] = $endDate ? max(0, $endDate->getTimestamp()) : 0;
3060+
3061+
return $derived;
30603062

3061-
if ($component->CLASS) {
3062-
$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
3063-
switch ($component->CLASS->getValue()) {
3064-
case 'PUBLIC':
3065-
$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
3066-
break;
3067-
case 'CONFIDENTIAL':
3068-
$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
3069-
break;
3070-
}
3071-
}
3072-
return [
3073-
'etag' => md5($calendarData),
3074-
'size' => strlen($calendarData),
3075-
'componentType' => $componentType,
3076-
'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
3077-
'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
3078-
'uid' => $uid,
3079-
'classification' => $classification
3080-
];
30813063
}
30823064

30833065
/**

0 commit comments

Comments
 (0)