Calendar Sync V2

This commit is contained in:
2026-03-11 12:18:02 +01:00
parent 2d5e9541c2
commit a4815fc672
6 changed files with 114 additions and 284 deletions

View File

@@ -4,24 +4,21 @@ namespace dokuwiki\plugin\luxtools;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTodo;
use Sabre\VObject\Reader;
use Throwable;
/**
* Write-back support for local ICS files.
*
* Handles updating event/task status (completion, reopening) in local
* ICS files while preserving the original component type and other properties.
* Handles updating event status (completion, reopening) in local
* ICS files while preserving other properties.
*/
class IcsWriter
{
/**
* Update the STATUS of an event or task occurrence in a local ICS file.
* Update the STATUS of an event occurrence in a local ICS file.
*
* For VEVENT: sets STATUS to the given value (TODO or COMPLETED).
* For VTODO: sets STATUS and, when completing, sets COMPLETED timestamp;
* when reopening, removes the COMPLETED property.
* Sets STATUS to the given value (TODO or COMPLETED).
*
* For recurring events, this writes an override/exception for the specific
* occurrence rather than modifying the master event.
@@ -108,15 +105,6 @@ class IcsWriter
}
}
// Try VTODO
foreach ($calendar->select('VTODO') as $component) {
if (!($component instanceof VTodo)) continue;
if (self::matchesComponent($component, $uid, $recurrenceId, $dateIso)) {
self::setVTodoStatus($component, $newStatus);
return true;
}
}
// For recurring events without a matching override, create one
foreach ($calendar->select('VEVENT') as $component) {
if (!($component instanceof VEvent)) continue;
@@ -131,25 +119,13 @@ class IcsWriter
if ($override !== null) return true;
}
// Same for VTODO with RRULE
foreach ($calendar->select('VTODO') as $component) {
if (!($component instanceof VTodo)) continue;
$componentUid = trim((string)($component->UID ?? ''));
if ($componentUid !== $uid) continue;
if (!isset($component->RRULE)) continue;
$override = self::createVTodoOccurrenceOverride($calendar, $component, $newStatus, $dateIso);
if ($override !== null) return true;
}
return false;
}
/**
* Check if a component matches the given UID and recurrence criteria.
*
* @param VEvent|VTodo $component
* @param VEvent $component
* @param string $uid
* @param string $recurrenceId
* @param string $dateIso
@@ -193,33 +169,6 @@ class IcsWriter
$vevent->STATUS = $newStatus;
}
/**
* Set the STATUS property on a VTODO with native completion semantics.
*
* @param VTodo $vtodo
* @param string $newStatus
*/
protected static function setVTodoStatus(VTodo $vtodo, string $newStatus): void
{
if ($newStatus === 'COMPLETED') {
$vtodo->STATUS = 'COMPLETED';
// Set COMPLETED timestamp per RFC 5545
$vtodo->COMPLETED = gmdate('Ymd\THis\Z');
// Set PERCENT-COMPLETE to 100
$vtodo->{'PERCENT-COMPLETE'} = '100';
} else {
// Reopening
$vtodo->STATUS = 'NEEDS-ACTION';
// Remove COMPLETED timestamp
if (isset($vtodo->COMPLETED)) {
unset($vtodo->COMPLETED);
}
if (isset($vtodo->{'PERCENT-COMPLETE'})) {
unset($vtodo->{'PERCENT-COMPLETE'});
}
}
}
/**
* Create an occurrence override for a recurring VEVENT.
*
@@ -300,97 +249,6 @@ class IcsWriter
}
}
/**
* Create an occurrence override for a recurring VTODO.
*
* @param VCalendar $calendar
* @param VTodo $master
* @param string $newStatus
* @param string $dateIso
* @return VTodo|null
*/
protected static function createVTodoOccurrenceOverride(
VCalendar $calendar,
VTodo $master,
string $newStatus,
string $dateIso
): ?VTodo {
try {
$dateProperty = $master->DUE ?? $master->DTSTART ?? null;
$isAllDay = $dateProperty !== null && strtoupper((string)($dateProperty['VALUE'] ?? '')) === 'DATE';
$props = [
'UID' => (string)$master->UID,
'SUMMARY' => (string)($master->SUMMARY ?? ''),
];
if ($isAllDay) {
$recurrenceValue = str_replace('-', '', $dateIso);
if (isset($master->DTSTART)) {
$props['DTSTART'] = $recurrenceValue;
}
if (isset($master->DUE)) {
$props['DUE'] = $recurrenceValue;
}
$props['RECURRENCE-ID'] = $recurrenceValue;
$override = $calendar->add('VTODO', $props);
if (isset($override->DTSTART)) {
$override->DTSTART['VALUE'] = 'DATE';
}
if (isset($override->DUE)) {
$override->DUE['VALUE'] = 'DATE';
}
$override->{'RECURRENCE-ID'}['VALUE'] = 'DATE';
} else {
$dt = $dateProperty->getDateTime();
$recurrenceValue = $dateIso . 'T' . $dt->format('His');
$tz = $dt->getTimezone();
if ($tz && $tz->getName() !== 'UTC') {
if (isset($master->DTSTART)) {
$props['DTSTART'] = $recurrenceValue;
}
if (isset($master->DUE)) {
$props['DUE'] = $recurrenceValue;
}
$props['RECURRENCE-ID'] = $recurrenceValue;
$override = $calendar->add('VTODO', $props);
if (isset($override->DTSTART)) {
$override->DTSTART['TZID'] = $tz->getName();
}
if (isset($override->DUE)) {
$override->DUE['TZID'] = $tz->getName();
}
$override->{'RECURRENCE-ID'}['TZID'] = $tz->getName();
} else {
$recurrenceValue .= 'Z';
if (isset($master->DTSTART)) {
$props['DTSTART'] = $recurrenceValue;
}
if (isset($master->DUE)) {
$props['DUE'] = $recurrenceValue;
}
$props['RECURRENCE-ID'] = $recurrenceValue;
$override = $calendar->add('VTODO', $props);
}
}
self::setVTodoStatus($override, $newStatus);
if (isset($master->LOCATION)) {
$override->LOCATION = (string)$master->LOCATION;
}
if (isset($master->DESCRIPTION)) {
$override->DESCRIPTION = (string)$master->DESCRIPTION;
}
return $override;
} catch (Throwable $e) {
return null;
}
}
/**
* Public atomic file write for use by CalDavClient sync.
*