Skip to content

Commit

Permalink
fix(caldav): improved data extraction for all component types
Browse files Browse the repository at this point in the history
Signed-off-by: SebastianKrupinski <[email protected]>
  • Loading branch information
miaulalala authored and SebastianKrupinski committed Feb 7, 2025
1 parent f21ffab commit 70385ee
Showing 1 changed file with 89 additions and 74 deletions.
163 changes: 89 additions & 74 deletions apps/dav/lib/CalDAV/CalDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -2985,99 +2985,114 @@ public function restoreChanges(int $calendarId, int $calendarType = self::CALEND
* @return array
*/
public function getDenormalizedData(string $calendarData): array {

$derived = [];
$derived['etag'] = md5($calendarData);
$derived['size'] = strlen($calendarData);
/** @var VCalendar $vObject */
$vObject = Reader::read($calendarData);
$vEvents = [];
$componentType = null;
$component = null;
$firstOccurrence = null;
$lastOccurrence = null;
$uid = null;
$classification = self::CLASSIFICATION_PUBLIC;
$hasDTSTART = false;
foreach ($vObject->getComponents() as $component) {
if ($component->name !== 'VTIMEZONE') {
// Finding all VEVENTs, and track them
if ($component->name === 'VEVENT') {
$vEvents[] = $component;
if ($component->DTSTART) {
$hasDTSTART = true;
}
}
// Track first component type and uid
if ($uid === null) {
$componentType = $component->name;
$uid = (string)$component->UID;
}
}

// validate data and extract base component
$components = $vObject->getBaseComponents();
if (count($components) === 0) {
throw new BadRequest('Invalid calendar object must containt either one VJOURNAL, VEVENT or VTODO component type');
}
if (!$componentType) {
throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
if (count($components) > 1) {
throw new BadRequest('Invalid calendar object must containt only one VJOURNAL, VEVENT or VTODO component type');
}
$component = $components[0];

if ($hasDTSTART) {
$component = $vEvents[0];

// Finding the last occurrence is a bit harder
if (!isset($component->RRULE) && count($vEvents) === 1) {
$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
if (isset($component->DTEND)) {
$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
} elseif (isset($component->DURATION)) {
$endDate = clone $component->DTSTART->getDateTime();
$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
$lastOccurrence = $endDate->getTimeStamp();
} elseif (!$component->DTSTART->hasTime()) {
$endDate = clone $component->DTSTART->getDateTime();
$endDate->modify('+1 day');
$lastOccurrence = $endDate->getTimeStamp();
} else {
$lastOccurrence = $firstOccurrence;
$derived['componentType'] = $component->name;
// Universal Id - All component types
if ($component->UID) {
$derived['uid'] = $component->UID->getValue();
}
// Visibility - VEVENT / VTODO / VJOURNAL component types
if ($component->CLASS) {
$derived['classification'] = match ($component->CLASS->getValue()) {
'PUBLIC' => CalDavBackend::CLASSIFICATION_PUBLIC,
'CONFIDENTIAL' => CalDavBackend::CLASSIFICATION_CONFIDENTIAL,
default => CalDavBackend::CLASSIFICATION_PRIVATE
};
} else {
$derived['classification'] = CalDavBackend::CLASSIFICATION_PUBLIC;
}
// Start Date - VEVENT / VTODO / VJOURNAL component types
// VTODO components can have no start date
if ($component->DTSTART) {
$startDate = $component->DTSTART->getDateTime();

Check failure on line 3023 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/dav/lib/CalDAV/CalDavBackend.php:3023:38: UndefinedMethod: Method Sabre\VObject\Property::getDateTime does not exist (see https://psalm.dev/022)
} else {
$startDate = null;
}
// End Date
if ($startDate !== null && $component->RRULE || $component->RDATE) { // Recurring
$endDate = clone $startDate;

Check failure on line 3029 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidClone

apps/dav/lib/CalDAV/CalDavBackend.php:3029:15: InvalidClone: Cannot clone null (see https://psalm.dev/069)
// Determine last absolute date
// RDATE can have both instances and multiple values
// RDATE;TZID=America/Toronto:20250701T000000,20260701T000000
// RDATE;TZID=America/Toronto:20270701T000000
if ($component->RDATE) {
foreach ($component->RDATE as $instance) {
foreach ($instance->getDateTimes() as $entry) {
if ($entry > $endDate) {
$endDate = $entry;
}
}
}
} else {
unset($instance, $entry);
}
// Determine last relative date
// RRULE can be infinate or limited by a UNTIL or COUNT
if ($component->RRULE) {
try {
$it = new EventIterator($vEvents);
$rule = new EventReaderRRule($component->RRULE->getValue(), $startDate);
} catch (NoInstancesException $e) {
$this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
'app' => 'dav',
'exception' => $e,
]);
throw new Forbidden($e->getMessage());
}
$maxDate = new DateTime(self::MAX_DATE);
$firstOccurrence = $it->getDtStart()->getTimestamp();
if ($it->isInfinite()) {
$lastOccurrence = $maxDate->getTimestamp();
if ($rule->isInfinite()) {
$endDate = new DateTime(self::MAX_DATE);
} else {
$end = $it->getDtEnd();
while ($it->valid() && $end < $maxDate) {
$end = $it->getDtEnd();
$it->next();
}
$lastOccurrence = $end->getTimestamp();
$endDate = $rule->concludes();
}
}
} elseif ($startDate !== null) { // Singleton
if ($component->DTEND) {
// VEVENT component types
$endDate = $component->DTEND->getDateTime();

Check failure on line 3065 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/dav/lib/CalDAV/CalDavBackend.php:3065:35: UndefinedMethod: Method Sabre\VObject\Property::getDateTime does not exist (see https://psalm.dev/022)
} elseif ($component->DURATION) {
// VEVENT / VTODO component types
$endDate = $startDate->add($component->DURATION->getDateInterval());

Check failure on line 3068 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/dav/lib/CalDAV/CalDavBackend.php:3068:54: UndefinedMethod: Method Sabre\VObject\Property::getDateInterval does not exist (see https://psalm.dev/022)
} elseif ($component->DUE) {
// VTODO component types
$endDate = $component->DUE->getDateTime();

Check failure on line 3071 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/dav/lib/CalDAV/CalDavBackend.php:3071:33: UndefinedMethod: Method Sabre\VObject\Property::getDateTime does not exist (see https://psalm.dev/022)
} elseif ($component->name === 'VEVENT' && !$component->DTSTART->hasTime()) {

Check failure on line 3072 in apps/dav/lib/CalDAV/CalDavBackend.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/dav/lib/CalDAV/CalDavBackend.php:3072:69: UndefinedMethod: Method Sabre\VObject\Property::hasTime does not exist (see https://psalm.dev/022)
// VEVENT component type without time is automatically one day
$endDate = (clone $startDate)->getDateTime()->modify('+1 day');
} else {
// fail safe, this should never happen but there is a tiny chance
$endDate = clone $startDate;
}
} else {
$endDate = null;
}

if ($component->CLASS) {
$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
switch ($component->CLASS->getValue()) {
case 'PUBLIC':
$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
break;
case 'CONFIDENTIAL':
$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
break;
}
$derived['firstOccurence'] = $startDate ? $startDate->getTimestamp() : 0;
$derived['lastOccurence'] = $endDate ? $endDate->getTimestamp() : 0;

// prevent negative start and end occurence
if ($derived['firstOccurence'] < 0) {
$derived['firstOccurence'] = 0;
}
return [
'etag' => md5($calendarData),
'size' => strlen($calendarData),
'componentType' => $componentType,
'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
'uid' => $uid,
'classification' => $classification
];
if ($derived['lastOccurence'] < 0) {
$derived['lastOccurence'] = 0;
}

return $derived;

}

/**
Expand Down

0 comments on commit 70385ee

Please sign in to comment.