|
68 | 68 | use Sabre\VObject\Component; |
69 | 69 | use Sabre\VObject\Component\VCalendar; |
70 | 70 | use Sabre\VObject\Component\VTimeZone; |
71 | | -use Sabre\VObject\DateTimeParser; |
72 | 71 | use Sabre\VObject\InvalidDataException; |
73 | 72 | use Sabre\VObject\ParseException; |
74 | 73 | use Sabre\VObject\Property; |
75 | 74 | use Sabre\VObject\Reader; |
76 | | -use Sabre\VObject\Recur\EventIterator; |
77 | 75 | use Sabre\VObject\Recur\MaxInstancesExceededException; |
78 | 76 | use Sabre\VObject\Recur\NoInstancesException; |
79 | 77 | use function array_column; |
@@ -3417,99 +3415,83 @@ public function restoreChanges(int $calendarId, int $calendarType = self::CALEND |
3417 | 3415 | * @return array |
3418 | 3416 | */ |
3419 | 3417 | 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 */ |
3420 | 3425 | $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 | + } |
3436 | 3458 | } |
3437 | 3459 | } |
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 | + } |
3467 | 3472 | } |
| 3473 | + // Singleton |
3468 | 3474 | } 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'); |
3489 | 3487 | } |
3490 | 3488 | } |
3491 | 3489 | } |
| 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; |
3492 | 3493 |
|
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; |
3513 | 3495 | } |
3514 | 3496 |
|
3515 | 3497 | /** |
|
0 commit comments