Calendar Sync V2
This commit is contained in:
@@ -359,8 +359,6 @@ Completed tasks can be reopened with a "Reopen" button.
|
||||
|
||||
Write-back rules:
|
||||
- `VEVENT` components: `STATUS:TODO` for open, `STATUS:COMPLETED` for completed.
|
||||
- `VTODO` components: Uses native completion semantics (`STATUS:COMPLETED`,
|
||||
`COMPLETED` timestamp, `PERCENT-COMPLETE:100`).
|
||||
- Recurring events: Completion writes an occurrence override/exception to preserve
|
||||
per-occurrence state rather than modifying the master event.
|
||||
|
||||
@@ -412,10 +410,6 @@ a cron job that triggers the sync via the DokuWiki AJAX endpoint.
|
||||
still has stale data. The completion write-back updates the remote immediately
|
||||
to mitigate this.
|
||||
|
||||
- **VTODO recurrence**: sabre/vobject's recurrence expansion has limited support
|
||||
for `VTODO` components. Recurring `VTODO` items may not expand as expected.
|
||||
Non-recurring `VTODO` items work correctly.
|
||||
|
||||
### 1) List files by glob pattern
|
||||
|
||||
The `{{directory>...}}` syntax (or `{{files>...}}` for backwards compatibility) can handle both directory listings and glob patterns. When a glob pattern is used, it renders as a table:
|
||||
|
||||
@@ -300,6 +300,37 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
echo '</label><br />';
|
||||
}
|
||||
|
||||
// CalDAV Sync button (outside the save form, separate action)
|
||||
$ajaxUrl = DOKU_BASE . 'lib/exe/ajax.php';
|
||||
$sectok = getSecurityToken();
|
||||
echo '<div class="luxtools-admin-sync" style="margin: 1em 0;">';
|
||||
echo '<button type="button" class="button" id="luxtools-sync-btn">'
|
||||
. hsc($this->getLang('calendar_sync_button'))
|
||||
. '</button>';
|
||||
echo '<span id="luxtools-sync-status" style="margin-left: 1em;"></span>';
|
||||
echo '</div>';
|
||||
echo '<script>';
|
||||
echo 'document.getElementById("luxtools-sync-btn").addEventListener("click", function() {';
|
||||
echo ' var btn = this;';
|
||||
echo ' var status = document.getElementById("luxtools-sync-status");';
|
||||
echo ' btn.disabled = true;';
|
||||
echo ' status.textContent = "Syncing...";';
|
||||
echo ' var xhr = new XMLHttpRequest();';
|
||||
echo ' xhr.open("POST", ' . json_encode($ajaxUrl) . ', true);';
|
||||
echo ' xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");';
|
||||
echo ' xhr.onload = function() {';
|
||||
echo ' btn.disabled = false;';
|
||||
echo ' try {';
|
||||
echo ' var r = JSON.parse(xhr.responseText);';
|
||||
echo ' status.textContent = r.message || (r.ok ? "Done" : "Failed");';
|
||||
echo ' status.style.color = r.ok ? "green" : "red";';
|
||||
echo ' } catch(e) { status.textContent = "Error"; status.style.color = "red"; }';
|
||||
echo ' };';
|
||||
echo ' xhr.onerror = function() { btn.disabled = false; status.textContent = "Network error"; status.style.color = "red"; };';
|
||||
echo ' xhr.send("call=luxtools_calendar_sync§ok=" + encodeURIComponent(' . json_encode($sectok) . '));';
|
||||
echo '});';
|
||||
echo '</script>';
|
||||
|
||||
// pagelink_search_depth
|
||||
echo '<label class="block"><span>' . hsc($this->getLang('pagelink_search_depth')) . '</span> ';
|
||||
echo '<input type="number" class="edit" min="0" name="pagelink_search_depth" value="' . hsc((string)$this->getConf('pagelink_search_depth')) . '" />';
|
||||
|
||||
@@ -147,32 +147,9 @@ class CalDavClient
|
||||
]);
|
||||
|
||||
if ($response === null) {
|
||||
// Try VTODO filter as well
|
||||
$body = '<?xml version="1.0" encoding="utf-8" ?>' .
|
||||
'<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">' .
|
||||
'<D:prop>' .
|
||||
'<D:getetag/>' .
|
||||
'<C:calendar-data/>' .
|
||||
'</D:prop>' .
|
||||
'<C:filter>' .
|
||||
'<C:comp-filter name="VCALENDAR">' .
|
||||
'<C:comp-filter name="VTODO">' .
|
||||
'<C:prop-filter name="UID">' .
|
||||
'<C:text-match collation="i;octet">' . htmlspecialchars($uid, ENT_XML1, 'UTF-8') . '</C:text-match>' .
|
||||
'</C:prop-filter>' .
|
||||
'</C:comp-filter>' .
|
||||
'</C:comp-filter>' .
|
||||
'</C:filter>' .
|
||||
'</C:calendar-query>';
|
||||
|
||||
$response = self::request('REPORT', $caldavUrl, $username, $password, $body, [
|
||||
'Content-Type: application/xml; charset=utf-8',
|
||||
'Depth: 1',
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($response === null) return null;
|
||||
|
||||
return self::parseReportResponse($response, $caldavUrl);
|
||||
}
|
||||
|
||||
@@ -245,13 +222,10 @@ class CalDavClient
|
||||
}
|
||||
}
|
||||
|
||||
// Copy VEVENT and VTODO components
|
||||
// Copy VEVENT components
|
||||
foreach ($cal->select('VEVENT') as $component) {
|
||||
$merged->add(clone $component);
|
||||
}
|
||||
foreach ($cal->select('VTODO') as $component) {
|
||||
$merged->add(clone $component);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Skip malformed objects
|
||||
continue;
|
||||
|
||||
@@ -47,7 +47,7 @@ class CalendarEvent
|
||||
*/
|
||||
public $status;
|
||||
|
||||
/** @var string Component type from source: VEVENT or VTODO */
|
||||
/** @var string Component type from source: VEVENT */
|
||||
public $componentType;
|
||||
|
||||
/** @var string The date (YYYY-MM-DD) this event applies to */
|
||||
|
||||
@@ -8,7 +8,6 @@ use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
use Sabre\VObject\Component\VEvent;
|
||||
use Sabre\VObject\Component\VTodo;
|
||||
use Sabre\VObject\Reader;
|
||||
use Throwable;
|
||||
|
||||
@@ -26,6 +25,9 @@ class CalendarService
|
||||
/** @var array<string,CalendarEvent[]> In-request cache keyed by "slotKey|all" for open tasks */
|
||||
protected static $taskCache = [];
|
||||
|
||||
/** @var array<string,VCalendar|null> In-request cache keyed by file path */
|
||||
protected static $vcalCache = [];
|
||||
|
||||
/**
|
||||
* Get all normalized events for a given date across all enabled slots.
|
||||
*
|
||||
@@ -163,6 +165,9 @@ class CalendarService
|
||||
/**
|
||||
* Get slot-level day indicator data for a whole month.
|
||||
*
|
||||
* Expands each slot's ICS calendar once for the full month range,
|
||||
* then buckets events by day — instead of 31 individual expand calls.
|
||||
*
|
||||
* @param CalendarSlot[] $slots
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
@@ -173,19 +178,76 @@ class CalendarService
|
||||
$daysInMonth = (int)date('t', mktime(0, 0, 0, $month, 1, $year));
|
||||
$indicators = [];
|
||||
|
||||
$utc = new DateTimeZone('UTC');
|
||||
// Expand from 1 day before month start to 1 day after month end
|
||||
$rangeStart = new DateTimeImmutable(sprintf('%04d-%02d-01 00:00:00', $year, $month), $utc);
|
||||
$rangeStart = $rangeStart->sub(new DateInterval('P1D'));
|
||||
$rangeEnd = $rangeStart->add(new DateInterval('P' . ($daysInMonth + 2) . 'D'));
|
||||
|
||||
foreach ($slots as $slot) {
|
||||
if (!$slot->isEnabled()) continue;
|
||||
$file = $slot->getFile();
|
||||
if ($file === '' || !is_file($file) || !is_readable($file)) continue;
|
||||
|
||||
$calendar = self::readCalendar($file);
|
||||
if ($calendar === null) continue;
|
||||
|
||||
try {
|
||||
$expanded = $calendar->expand($rangeStart, $rangeEnd);
|
||||
if (!($expanded instanceof VCalendar)) continue;
|
||||
|
||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||
$dateIso = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
if (self::slotHasEventsOnDate($slot, $dateIso)) {
|
||||
$events = self::collectFromCalendar($expanded, $slot->getKey(), $dateIso);
|
||||
if ($events !== []) {
|
||||
$indicators[$dateIso][] = $slot->getKey();
|
||||
// Pre-populate the day cache
|
||||
$cacheKey = $slot->getKey() . '|' . $dateIso;
|
||||
if (!isset(self::$dayCache[$cacheKey])) {
|
||||
self::$dayCache[$cacheKey] = $events;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $indicators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse an ICS file, caching the parsed VCalendar per file path.
|
||||
*
|
||||
* @param string $file
|
||||
* @return VCalendar|null
|
||||
*/
|
||||
protected static function readCalendar(string $file): ?VCalendar
|
||||
{
|
||||
if (array_key_exists($file, self::$vcalCache)) {
|
||||
return self::$vcalCache[$file];
|
||||
}
|
||||
|
||||
$raw = @file_get_contents($file);
|
||||
if (!is_string($raw) || trim($raw) === '') {
|
||||
self::$vcalCache[$file] = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$component = Reader::read($raw, Reader::OPTION_FORGIVING);
|
||||
if (!($component instanceof VCalendar)) {
|
||||
self::$vcalCache[$file] = null;
|
||||
return null;
|
||||
}
|
||||
self::$vcalCache[$file] = $component;
|
||||
return $component;
|
||||
} catch (Throwable $e) {
|
||||
self::$vcalCache[$file] = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse events from a local ICS file for a specific date.
|
||||
*
|
||||
@@ -196,19 +258,16 @@ class CalendarService
|
||||
*/
|
||||
protected static function parseEventsFromFile(string $file, string $slotKey, string $dateIso): array
|
||||
{
|
||||
$raw = @file_get_contents($file);
|
||||
if (!is_string($raw) || trim($raw) === '') return [];
|
||||
$calendar = self::readCalendar($file);
|
||||
if ($calendar === null) return [];
|
||||
|
||||
try {
|
||||
$component = Reader::read($raw, Reader::OPTION_FORGIVING);
|
||||
if (!($component instanceof VCalendar)) return [];
|
||||
|
||||
$utc = new DateTimeZone('UTC');
|
||||
$rangeStart = new DateTimeImmutable($dateIso . ' 00:00:00', $utc);
|
||||
$rangeStart = $rangeStart->sub(new DateInterval('P1D'));
|
||||
$rangeEnd = $rangeStart->add(new DateInterval('P3D'));
|
||||
|
||||
$expanded = $component->expand($rangeStart, $rangeEnd);
|
||||
$expanded = $calendar->expand($rangeStart, $rangeEnd);
|
||||
if (!($expanded instanceof VCalendar)) return [];
|
||||
|
||||
return self::collectFromCalendar($expanded, $slotKey, $dateIso);
|
||||
@@ -218,7 +277,7 @@ class CalendarService
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse all tasks (VEVENT with STATUS or VTODO) from a maintenance file,
|
||||
* Parse all tasks (VEVENT with STATUS) from a maintenance file,
|
||||
* expanding recurrences up to the given date.
|
||||
*
|
||||
* @param string $file
|
||||
@@ -228,12 +287,11 @@ class CalendarService
|
||||
*/
|
||||
protected static function parseAllTasksFromFile(string $file, string $slotKey, string $todayIso): array
|
||||
{
|
||||
$raw = @file_get_contents($file);
|
||||
if (!is_string($raw) || trim($raw) === '') return [];
|
||||
$calendar = self::readCalendar($file);
|
||||
if ($calendar === null) return [];
|
||||
|
||||
try {
|
||||
$component = Reader::read($raw, Reader::OPTION_FORGIVING);
|
||||
if (!($component instanceof VCalendar)) return [];
|
||||
$component = $calendar;
|
||||
|
||||
// Expand from a reasonable lookback to tomorrow
|
||||
$utc = new DateTimeZone('UTC');
|
||||
@@ -255,15 +313,6 @@ class CalendarService
|
||||
}
|
||||
}
|
||||
|
||||
// Collect VTODOs
|
||||
foreach ($expanded->select('VTODO') as $vtodo) {
|
||||
if (!($vtodo instanceof VTodo)) continue;
|
||||
$event = self::normalizeVTodo($vtodo, $slotKey);
|
||||
if ($event !== null) {
|
||||
$tasks[] = $event;
|
||||
}
|
||||
}
|
||||
|
||||
return $tasks;
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
@@ -295,18 +344,6 @@ class CalendarService
|
||||
$result[] = $event;
|
||||
}
|
||||
|
||||
// VTODOs
|
||||
foreach ($calendar->select('VTODO') as $vtodo) {
|
||||
if (!($vtodo instanceof VTodo)) continue;
|
||||
$event = self::normalizeVTodoForDay($vtodo, $slotKey, $dateIso);
|
||||
if ($event === null) continue;
|
||||
|
||||
$dedupeKey = $event->uid . '|' . $event->recurrenceId . '|' . $event->startIso . '|' . $event->summary;
|
||||
if (isset($seen[$dedupeKey])) continue;
|
||||
$seen[$dedupeKey] = true;
|
||||
$result[] = $event;
|
||||
}
|
||||
|
||||
usort($result, [self::class, 'compareEvents']);
|
||||
return $result;
|
||||
}
|
||||
@@ -386,71 +423,6 @@ class CalendarService
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a VTODO for a specific day.
|
||||
*
|
||||
* @param VTodo $vtodo
|
||||
* @param string $slotKey
|
||||
* @param string $dateIso
|
||||
* @return CalendarEvent|null
|
||||
*/
|
||||
protected static function normalizeVTodoForDay(VTodo $vtodo, string $slotKey, string $dateIso): ?CalendarEvent
|
||||
{
|
||||
$event = self::normalizeVTodo($vtodo, $slotKey);
|
||||
if ($event === null) return null;
|
||||
|
||||
// Check if the VTODO's due/start date matches this day
|
||||
if ($event->dateIso !== $dateIso) return null;
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a VTODO into a CalendarEvent.
|
||||
*
|
||||
* @param VTodo $vtodo
|
||||
* @param string $slotKey
|
||||
* @return CalendarEvent|null
|
||||
*/
|
||||
protected static function normalizeVTodo(VTodo $vtodo, string $slotKey): ?CalendarEvent
|
||||
{
|
||||
// VTODO uses DUE or DTSTART for date
|
||||
$dateProperty = $vtodo->DUE ?? $vtodo->DTSTART ?? null;
|
||||
if ($dateProperty === null) return null;
|
||||
|
||||
$isAllDay = strtoupper((string)($dateProperty['VALUE'] ?? '')) === 'DATE';
|
||||
$dt = self::toImmutable($dateProperty->getDateTime());
|
||||
if ($dt === null) return null;
|
||||
|
||||
$event = new CalendarEvent();
|
||||
$event->slotKey = $slotKey;
|
||||
$event->uid = trim((string)($vtodo->UID ?? ''));
|
||||
$event->recurrenceId = isset($vtodo->{'RECURRENCE-ID'}) ? trim((string)$vtodo->{'RECURRENCE-ID'}) : '';
|
||||
$event->summary = trim((string)($vtodo->SUMMARY ?? ''));
|
||||
if ($event->summary === '') $event->summary = '(ohne Titel)';
|
||||
$event->startIso = $dt->format(DateTimeInterface::ATOM);
|
||||
$event->endIso = '';
|
||||
$event->allDay = $isAllDay;
|
||||
$event->time = $isAllDay ? '' : $dt->format('H:i');
|
||||
$event->location = trim((string)($vtodo->LOCATION ?? ''));
|
||||
$event->description = trim((string)($vtodo->DESCRIPTION ?? ''));
|
||||
|
||||
$status = strtoupper(trim((string)($vtodo->STATUS ?? '')));
|
||||
// Map VTODO statuses to our model
|
||||
if ($status === 'COMPLETED') {
|
||||
$event->status = 'COMPLETED';
|
||||
} elseif ($status === 'IN-PROCESS' || $status === 'NEEDS-ACTION' || $status === '') {
|
||||
$event->status = 'TODO';
|
||||
} else {
|
||||
$event->status = $status;
|
||||
}
|
||||
|
||||
$event->componentType = 'VTODO';
|
||||
$event->dateIso = $dt->format('Y-m-d');
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the end date/time for a VEVENT.
|
||||
*
|
||||
@@ -549,5 +521,6 @@ class CalendarService
|
||||
{
|
||||
self::$dayCache = [];
|
||||
self::$taskCache = [];
|
||||
self::$vcalCache = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user