119378907SAtari911<?php 219378907SAtari911/** 319378907SAtari911 * DokuWiki Plugin calendar (Action Component) 419378907SAtari911 * 519378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 619378907SAtari911 * @author DokuWiki Community 719378907SAtari911 */ 819378907SAtari911 919378907SAtari911if (!defined('DOKU_INC')) die(); 1019378907SAtari911 1119378907SAtari911class action_plugin_calendar extends DokuWiki_Action_Plugin { 1219378907SAtari911 1319378907SAtari911 public function register(Doku_Event_Handler $controller) { 1419378907SAtari911 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax'); 1519378907SAtari911 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addAssets'); 1619378907SAtari911 } 1719378907SAtari911 1819378907SAtari911 public function handleAjax(Doku_Event $event, $param) { 1919378907SAtari911 if ($event->data !== 'plugin_calendar') return; 2019378907SAtari911 $event->preventDefault(); 2119378907SAtari911 $event->stopPropagation(); 2219378907SAtari911 2319378907SAtari911 $action = $_REQUEST['action'] ?? ''; 2419378907SAtari911 2519378907SAtari911 switch ($action) { 2619378907SAtari911 case 'save_event': 2719378907SAtari911 $this->saveEvent(); 2819378907SAtari911 break; 2919378907SAtari911 case 'delete_event': 3019378907SAtari911 $this->deleteEvent(); 3119378907SAtari911 break; 3219378907SAtari911 case 'get_event': 3319378907SAtari911 $this->getEvent(); 3419378907SAtari911 break; 3519378907SAtari911 case 'load_month': 3619378907SAtari911 $this->loadMonth(); 3719378907SAtari911 break; 3819378907SAtari911 case 'toggle_task': 3919378907SAtari911 $this->toggleTaskComplete(); 4019378907SAtari911 break; 4119378907SAtari911 default: 4219378907SAtari911 echo json_encode(['success' => false, 'error' => 'Unknown action']); 4319378907SAtari911 } 4419378907SAtari911 } 4519378907SAtari911 4619378907SAtari911 private function saveEvent() { 4719378907SAtari911 global $INPUT; 4819378907SAtari911 4919378907SAtari911 $namespace = $INPUT->str('namespace', ''); 5019378907SAtari911 $date = $INPUT->str('date'); 5119378907SAtari911 $eventId = $INPUT->str('eventId', ''); 5219378907SAtari911 $title = $INPUT->str('title'); 5319378907SAtari911 $time = $INPUT->str('time', ''); 541d05cddcSAtari911 $endTime = $INPUT->str('endTime', ''); 5519378907SAtari911 $description = $INPUT->str('description', ''); 5619378907SAtari911 $color = $INPUT->str('color', '#3498db'); 5719378907SAtari911 $oldDate = $INPUT->str('oldDate', ''); // Track original date for moves 5819378907SAtari911 $isTask = $INPUT->bool('isTask', false); 5919378907SAtari911 $completed = $INPUT->bool('completed', false); 6019378907SAtari911 $endDate = $INPUT->str('endDate', ''); 6187ac9bf3SAtari911 $isRecurring = $INPUT->bool('isRecurring', false); 6287ac9bf3SAtari911 $recurrenceType = $INPUT->str('recurrenceType', 'weekly'); 6387ac9bf3SAtari911 $recurrenceEnd = $INPUT->str('recurrenceEnd', ''); 6419378907SAtari911 6519378907SAtari911 if (!$date || !$title) { 6619378907SAtari911 echo json_encode(['success' => false, 'error' => 'Missing required fields']); 6719378907SAtari911 return; 6819378907SAtari911 } 6919378907SAtari911 701d05cddcSAtari911 // If editing, find the event's stored namespace (for finding/deleting old event) 71e3a9f44cSAtari911 $storedNamespace = ''; 721d05cddcSAtari911 $oldNamespace = ''; 73e3a9f44cSAtari911 if ($eventId) { 741d05cddcSAtari911 // Use oldDate if available (date was changed), otherwise use current date 751d05cddcSAtari911 $searchDate = ($oldDate && $oldDate !== $date) ? $oldDate : $date; 761d05cddcSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $searchDate, $namespace); 771d05cddcSAtari911 781d05cddcSAtari911 // Store the old namespace for deletion purposes 791d05cddcSAtari911 if ($storedNamespace !== null) { 801d05cddcSAtari911 $oldNamespace = $storedNamespace; 811d05cddcSAtari911 error_log("Calendar saveEvent: Found existing event in namespace '$oldNamespace'"); 821d05cddcSAtari911 } 83e3a9f44cSAtari911 } 84e3a9f44cSAtari911 851d05cddcSAtari911 // Use the namespace provided by the user (allow namespace changes!) 861d05cddcSAtari911 // But normalize wildcards and multi-namespace to empty for NEW events 871d05cddcSAtari911 if (!$eventId) { 881d05cddcSAtari911 error_log("Calendar saveEvent: NEW event, received namespace='$namespace'"); 89e3a9f44cSAtari911 // Normalize namespace: treat wildcards and multi-namespace as empty (default) for NEW events 90e3a9f44cSAtari911 if (!empty($namespace) && (strpos($namespace, '*') !== false || strpos($namespace, ';') !== false)) { 911d05cddcSAtari911 error_log("Calendar saveEvent: Namespace contains wildcard/multi, clearing to empty"); 92e3a9f44cSAtari911 $namespace = ''; 931d05cddcSAtari911 } else { 941d05cddcSAtari911 error_log("Calendar saveEvent: Namespace is clean, keeping as '$namespace'"); 95e3a9f44cSAtari911 } 961d05cddcSAtari911 } else { 971d05cddcSAtari911 error_log("Calendar saveEvent: EDITING event $eventId, user selected namespace='$namespace'"); 98e3a9f44cSAtari911 } 99e3a9f44cSAtari911 10087ac9bf3SAtari911 // Generate event ID if new 10187ac9bf3SAtari911 $generatedId = $eventId ?: uniqid(); 10287ac9bf3SAtari911 103*9ccd446eSAtari911 // If editing a recurring event, load existing data to preserve unchanged fields 104*9ccd446eSAtari911 $existingEventData = null; 105*9ccd446eSAtari911 if ($eventId && $isRecurring) { 106*9ccd446eSAtari911 $searchDate = ($oldDate && $oldDate !== $date) ? $oldDate : $date; 107*9ccd446eSAtari911 $existingEventData = $this->getExistingEventData($eventId, $searchDate, $oldNamespace ?: $namespace); 108*9ccd446eSAtari911 if ($existingEventData) { 109*9ccd446eSAtari911 error_log("Calendar saveEvent recurring: Loaded existing data - namespace='" . ($existingEventData['namespace'] ?? 'NOT SET') . "'"); 110*9ccd446eSAtari911 } 111*9ccd446eSAtari911 } 112*9ccd446eSAtari911 11387ac9bf3SAtari911 // If recurring, generate multiple events 11487ac9bf3SAtari911 if ($isRecurring) { 115*9ccd446eSAtari911 // Merge with existing data if editing (preserve values that weren't changed) 116*9ccd446eSAtari911 if ($existingEventData) { 117*9ccd446eSAtari911 $title = $title ?: $existingEventData['title']; 118*9ccd446eSAtari911 $time = $time ?: (isset($existingEventData['time']) ? $existingEventData['time'] : ''); 119*9ccd446eSAtari911 $endTime = $endTime ?: (isset($existingEventData['endTime']) ? $existingEventData['endTime'] : ''); 120*9ccd446eSAtari911 $description = $description ?: (isset($existingEventData['description']) ? $existingEventData['description'] : ''); 121*9ccd446eSAtari911 // Only use existing color if new color is default 122*9ccd446eSAtari911 if ($color === '#3498db' && isset($existingEventData['color'])) { 123*9ccd446eSAtari911 $color = $existingEventData['color']; 124*9ccd446eSAtari911 } 125*9ccd446eSAtari911 126*9ccd446eSAtari911 // Preserve namespace in these cases: 127*9ccd446eSAtari911 // 1. Namespace field is empty (user didn't select anything) 128*9ccd446eSAtari911 // 2. Namespace contains wildcards (like "personal;work" or "work*") 129*9ccd446eSAtari911 // 3. Namespace is the same as what was passed (no change intended) 130*9ccd446eSAtari911 $receivedNamespace = $namespace; 131*9ccd446eSAtari911 if (empty($namespace) || strpos($namespace, '*') !== false || strpos($namespace, ';') !== false) { 132*9ccd446eSAtari911 if (isset($existingEventData['namespace'])) { 133*9ccd446eSAtari911 $namespace = $existingEventData['namespace']; 134*9ccd446eSAtari911 error_log("Calendar saveEvent recurring: Preserving namespace '$namespace' (received='$receivedNamespace')"); 135*9ccd446eSAtari911 } else { 136*9ccd446eSAtari911 error_log("Calendar saveEvent recurring: No existing namespace to preserve (received='$receivedNamespace')"); 137*9ccd446eSAtari911 } 138*9ccd446eSAtari911 } else { 139*9ccd446eSAtari911 error_log("Calendar saveEvent recurring: Using new namespace '$namespace' (received='$receivedNamespace')"); 140*9ccd446eSAtari911 } 141*9ccd446eSAtari911 } else { 142*9ccd446eSAtari911 error_log("Calendar saveEvent recurring: No existing data found, using namespace='$namespace'"); 143*9ccd446eSAtari911 } 144*9ccd446eSAtari911 14587ac9bf3SAtari911 $this->createRecurringEvents($namespace, $date, $endDate, $title, $time, $description, 14687ac9bf3SAtari911 $color, $isTask, $recurrenceType, $recurrenceEnd, $generatedId); 14787ac9bf3SAtari911 echo json_encode(['success' => true]); 14887ac9bf3SAtari911 return; 14987ac9bf3SAtari911 } 15087ac9bf3SAtari911 15119378907SAtari911 list($year, $month, $day) = explode('-', $date); 15219378907SAtari911 1531d05cddcSAtari911 // NEW namespace directory (where we'll save) 15419378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 15519378907SAtari911 if ($namespace) { 15619378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 15719378907SAtari911 } 15819378907SAtari911 $dataDir .= 'calendar/'; 15919378907SAtari911 16019378907SAtari911 if (!is_dir($dataDir)) { 16119378907SAtari911 mkdir($dataDir, 0755, true); 16219378907SAtari911 } 16319378907SAtari911 16419378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 16519378907SAtari911 16619378907SAtari911 $events = []; 16719378907SAtari911 if (file_exists($eventFile)) { 16819378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 16919378907SAtari911 } 17019378907SAtari911 1711d05cddcSAtari911 // If editing and (date changed OR namespace changed), remove from old location first 1721d05cddcSAtari911 $namespaceChanged = ($eventId && $oldNamespace !== '' && $oldNamespace !== $namespace); 1731d05cddcSAtari911 $dateChanged = ($eventId && $oldDate && $oldDate !== $date); 1741d05cddcSAtari911 1751d05cddcSAtari911 if ($namespaceChanged || $dateChanged) { 1761d05cddcSAtari911 // Construct OLD data directory using OLD namespace 1771d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/'; 1781d05cddcSAtari911 if ($oldNamespace) { 1791d05cddcSAtari911 $oldDataDir .= str_replace(':', '/', $oldNamespace) . '/'; 1801d05cddcSAtari911 } 1811d05cddcSAtari911 $oldDataDir .= 'calendar/'; 1821d05cddcSAtari911 1831d05cddcSAtari911 $deleteDate = $dateChanged ? $oldDate : $date; 1841d05cddcSAtari911 list($oldYear, $oldMonth, $oldDay) = explode('-', $deleteDate); 1851d05cddcSAtari911 $oldEventFile = $oldDataDir . sprintf('%04d-%02d.json', $oldYear, $oldMonth); 18619378907SAtari911 18719378907SAtari911 if (file_exists($oldEventFile)) { 18819378907SAtari911 $oldEvents = json_decode(file_get_contents($oldEventFile), true); 1891d05cddcSAtari911 if (isset($oldEvents[$deleteDate])) { 1901d05cddcSAtari911 $oldEvents[$deleteDate] = array_values(array_filter($oldEvents[$deleteDate], function($evt) use ($eventId) { 19119378907SAtari911 return $evt['id'] !== $eventId; 192e3a9f44cSAtari911 })); 19319378907SAtari911 1941d05cddcSAtari911 if (empty($oldEvents[$deleteDate])) { 1951d05cddcSAtari911 unset($oldEvents[$deleteDate]); 19619378907SAtari911 } 19719378907SAtari911 19819378907SAtari911 file_put_contents($oldEventFile, json_encode($oldEvents, JSON_PRETTY_PRINT)); 1991d05cddcSAtari911 error_log("Calendar saveEvent: Deleted event from old location - namespace:'$oldNamespace', date:'$deleteDate'"); 20019378907SAtari911 } 20119378907SAtari911 } 20219378907SAtari911 } 20319378907SAtari911 20419378907SAtari911 if (!isset($events[$date])) { 20519378907SAtari911 $events[$date] = []; 206e3a9f44cSAtari911 } elseif (!is_array($events[$date])) { 207e3a9f44cSAtari911 // Fix corrupted data - ensure it's an array 208e3a9f44cSAtari911 error_log("Calendar saveEvent: Fixing corrupted data at $date - was not an array"); 209e3a9f44cSAtari911 $events[$date] = []; 21019378907SAtari911 } 21119378907SAtari911 212e3a9f44cSAtari911 // Store the namespace with the event 21319378907SAtari911 $eventData = [ 21487ac9bf3SAtari911 'id' => $generatedId, 21519378907SAtari911 'title' => $title, 21619378907SAtari911 'time' => $time, 2171d05cddcSAtari911 'endTime' => $endTime, 21819378907SAtari911 'description' => $description, 21919378907SAtari911 'color' => $color, 22019378907SAtari911 'isTask' => $isTask, 22119378907SAtari911 'completed' => $completed, 22219378907SAtari911 'endDate' => $endDate, 223e3a9f44cSAtari911 'namespace' => $namespace, // Store namespace with event 22419378907SAtari911 'created' => date('Y-m-d H:i:s') 22519378907SAtari911 ]; 22619378907SAtari911 2271d05cddcSAtari911 // Debug logging 2281d05cddcSAtari911 error_log("Calendar saveEvent: Saving event '$title' with namespace='$namespace' to file $eventFile"); 2291d05cddcSAtari911 23019378907SAtari911 // If editing, replace existing event 23119378907SAtari911 if ($eventId) { 23219378907SAtari911 $found = false; 23319378907SAtari911 foreach ($events[$date] as $key => $evt) { 23419378907SAtari911 if ($evt['id'] === $eventId) { 23519378907SAtari911 $events[$date][$key] = $eventData; 23619378907SAtari911 $found = true; 23719378907SAtari911 break; 23819378907SAtari911 } 23919378907SAtari911 } 24019378907SAtari911 if (!$found) { 24119378907SAtari911 $events[$date][] = $eventData; 24219378907SAtari911 } 24319378907SAtari911 } else { 24419378907SAtari911 $events[$date][] = $eventData; 24519378907SAtari911 } 24619378907SAtari911 24719378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 24819378907SAtari911 249e3a9f44cSAtari911 // If event spans multiple months, add it to the first day of each subsequent month 250e3a9f44cSAtari911 if ($endDate && $endDate !== $date) { 251e3a9f44cSAtari911 $startDateObj = new DateTime($date); 252e3a9f44cSAtari911 $endDateObj = new DateTime($endDate); 253e3a9f44cSAtari911 254e3a9f44cSAtari911 // Get the month/year of the start date 255e3a9f44cSAtari911 $startMonth = $startDateObj->format('Y-m'); 256e3a9f44cSAtari911 257e3a9f44cSAtari911 // Iterate through each month the event spans 258e3a9f44cSAtari911 $currentDate = clone $startDateObj; 259e3a9f44cSAtari911 $currentDate->modify('first day of next month'); // Jump to first of next month 260e3a9f44cSAtari911 261e3a9f44cSAtari911 while ($currentDate <= $endDateObj) { 262e3a9f44cSAtari911 $currentMonth = $currentDate->format('Y-m'); 263e3a9f44cSAtari911 $firstDayOfMonth = $currentDate->format('Y-m-01'); 264e3a9f44cSAtari911 265e3a9f44cSAtari911 list($currentYear, $currentMonthNum, $currentDay) = explode('-', $firstDayOfMonth); 266e3a9f44cSAtari911 267e3a9f44cSAtari911 // Get the file for this month 268e3a9f44cSAtari911 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonthNum); 269e3a9f44cSAtari911 270e3a9f44cSAtari911 $currentEvents = []; 271e3a9f44cSAtari911 if (file_exists($currentEventFile)) { 272e3a9f44cSAtari911 $contents = file_get_contents($currentEventFile); 273e3a9f44cSAtari911 $decoded = json_decode($contents, true); 274e3a9f44cSAtari911 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { 275e3a9f44cSAtari911 $currentEvents = $decoded; 276e3a9f44cSAtari911 } else { 277e3a9f44cSAtari911 error_log("Calendar saveEvent: JSON decode error in $currentEventFile: " . json_last_error_msg()); 278e3a9f44cSAtari911 } 279e3a9f44cSAtari911 } 280e3a9f44cSAtari911 281e3a9f44cSAtari911 // Add entry for the first day of this month 282e3a9f44cSAtari911 if (!isset($currentEvents[$firstDayOfMonth])) { 283e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = []; 284e3a9f44cSAtari911 } elseif (!is_array($currentEvents[$firstDayOfMonth])) { 285e3a9f44cSAtari911 // Fix corrupted data - ensure it's an array 286e3a9f44cSAtari911 error_log("Calendar saveEvent: Fixing corrupted data at $firstDayOfMonth - was not an array"); 287e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = []; 288e3a9f44cSAtari911 } 289e3a9f44cSAtari911 290e3a9f44cSAtari911 // Create a copy with the original start date preserved 291e3a9f44cSAtari911 $eventDataForMonth = $eventData; 292e3a9f44cSAtari911 $eventDataForMonth['originalStartDate'] = $date; // Preserve the actual start date 293e3a9f44cSAtari911 294e3a9f44cSAtari911 // Check if event already exists (when editing) 295e3a9f44cSAtari911 $found = false; 296e3a9f44cSAtari911 if ($eventId) { 297e3a9f44cSAtari911 foreach ($currentEvents[$firstDayOfMonth] as $key => $evt) { 298e3a9f44cSAtari911 if ($evt['id'] === $eventId) { 299e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth][$key] = $eventDataForMonth; 300e3a9f44cSAtari911 $found = true; 301e3a9f44cSAtari911 break; 302e3a9f44cSAtari911 } 303e3a9f44cSAtari911 } 304e3a9f44cSAtari911 } 305e3a9f44cSAtari911 306e3a9f44cSAtari911 if (!$found) { 307e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth][] = $eventDataForMonth; 308e3a9f44cSAtari911 } 309e3a9f44cSAtari911 310e3a9f44cSAtari911 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 311e3a9f44cSAtari911 312e3a9f44cSAtari911 // Move to next month 313e3a9f44cSAtari911 $currentDate->modify('first day of next month'); 314e3a9f44cSAtari911 } 315e3a9f44cSAtari911 } 316e3a9f44cSAtari911 31719378907SAtari911 echo json_encode(['success' => true, 'events' => $events, 'eventId' => $eventData['id']]); 31819378907SAtari911 } 31919378907SAtari911 32019378907SAtari911 private function deleteEvent() { 32119378907SAtari911 global $INPUT; 32219378907SAtari911 32319378907SAtari911 $namespace = $INPUT->str('namespace', ''); 32419378907SAtari911 $date = $INPUT->str('date'); 32519378907SAtari911 $eventId = $INPUT->str('eventId'); 32619378907SAtari911 327e3a9f44cSAtari911 // Find where the event actually lives 328e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 329e3a9f44cSAtari911 330e3a9f44cSAtari911 if ($storedNamespace === null) { 331e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 332e3a9f44cSAtari911 return; 333e3a9f44cSAtari911 } 334e3a9f44cSAtari911 335e3a9f44cSAtari911 // Use the found namespace 336e3a9f44cSAtari911 $namespace = $storedNamespace; 337e3a9f44cSAtari911 33819378907SAtari911 list($year, $month, $day) = explode('-', $date); 33919378907SAtari911 34019378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 34119378907SAtari911 if ($namespace) { 34219378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 34319378907SAtari911 } 34419378907SAtari911 $dataDir .= 'calendar/'; 34519378907SAtari911 34619378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 34719378907SAtari911 348*9ccd446eSAtari911 // First, get the event to check if it spans multiple months or is recurring 349e3a9f44cSAtari911 $eventToDelete = null; 350*9ccd446eSAtari911 $isRecurring = false; 351*9ccd446eSAtari911 $recurringId = null; 352*9ccd446eSAtari911 35319378907SAtari911 if (file_exists($eventFile)) { 35419378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 35519378907SAtari911 35619378907SAtari911 if (isset($events[$date])) { 357e3a9f44cSAtari911 foreach ($events[$date] as $event) { 358e3a9f44cSAtari911 if ($event['id'] === $eventId) { 359e3a9f44cSAtari911 $eventToDelete = $event; 360*9ccd446eSAtari911 $isRecurring = isset($event['recurring']) && $event['recurring']; 361*9ccd446eSAtari911 $recurringId = isset($event['recurringId']) ? $event['recurringId'] : null; 362e3a9f44cSAtari911 break; 363e3a9f44cSAtari911 } 364e3a9f44cSAtari911 } 365e3a9f44cSAtari911 366e3a9f44cSAtari911 $events[$date] = array_values(array_filter($events[$date], function($event) use ($eventId) { 36719378907SAtari911 return $event['id'] !== $eventId; 368e3a9f44cSAtari911 })); 36919378907SAtari911 37019378907SAtari911 if (empty($events[$date])) { 37119378907SAtari911 unset($events[$date]); 37219378907SAtari911 } 37319378907SAtari911 37419378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 37519378907SAtari911 } 37619378907SAtari911 } 37719378907SAtari911 378*9ccd446eSAtari911 // If this is a recurring event, delete ALL occurrences with the same recurringId 379*9ccd446eSAtari911 if ($isRecurring && $recurringId) { 380*9ccd446eSAtari911 $this->deleteAllRecurringInstances($recurringId, $namespace, $dataDir); 381*9ccd446eSAtari911 } 382*9ccd446eSAtari911 383e3a9f44cSAtari911 // If event spans multiple months, delete it from the first day of each subsequent month 384e3a9f44cSAtari911 if ($eventToDelete && isset($eventToDelete['endDate']) && $eventToDelete['endDate'] && $eventToDelete['endDate'] !== $date) { 385e3a9f44cSAtari911 $startDateObj = new DateTime($date); 386e3a9f44cSAtari911 $endDateObj = new DateTime($eventToDelete['endDate']); 387e3a9f44cSAtari911 388e3a9f44cSAtari911 // Iterate through each month the event spans 389e3a9f44cSAtari911 $currentDate = clone $startDateObj; 390e3a9f44cSAtari911 $currentDate->modify('first day of next month'); // Jump to first of next month 391e3a9f44cSAtari911 392e3a9f44cSAtari911 while ($currentDate <= $endDateObj) { 393e3a9f44cSAtari911 $firstDayOfMonth = $currentDate->format('Y-m-01'); 394e3a9f44cSAtari911 list($currentYear, $currentMonth, $currentDay) = explode('-', $firstDayOfMonth); 395e3a9f44cSAtari911 396e3a9f44cSAtari911 // Get the file for this month 397e3a9f44cSAtari911 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonth); 398e3a9f44cSAtari911 399e3a9f44cSAtari911 if (file_exists($currentEventFile)) { 400e3a9f44cSAtari911 $currentEvents = json_decode(file_get_contents($currentEventFile), true); 401e3a9f44cSAtari911 402e3a9f44cSAtari911 if (isset($currentEvents[$firstDayOfMonth])) { 403e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = array_values(array_filter($currentEvents[$firstDayOfMonth], function($event) use ($eventId) { 404e3a9f44cSAtari911 return $event['id'] !== $eventId; 405e3a9f44cSAtari911 })); 406e3a9f44cSAtari911 407e3a9f44cSAtari911 if (empty($currentEvents[$firstDayOfMonth])) { 408e3a9f44cSAtari911 unset($currentEvents[$firstDayOfMonth]); 409e3a9f44cSAtari911 } 410e3a9f44cSAtari911 411e3a9f44cSAtari911 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 412e3a9f44cSAtari911 } 413e3a9f44cSAtari911 } 414e3a9f44cSAtari911 415e3a9f44cSAtari911 // Move to next month 416e3a9f44cSAtari911 $currentDate->modify('first day of next month'); 417e3a9f44cSAtari911 } 418e3a9f44cSAtari911 } 419e3a9f44cSAtari911 42019378907SAtari911 echo json_encode(['success' => true]); 42119378907SAtari911 } 42219378907SAtari911 42319378907SAtari911 private function getEvent() { 42419378907SAtari911 global $INPUT; 42519378907SAtari911 42619378907SAtari911 $namespace = $INPUT->str('namespace', ''); 42719378907SAtari911 $date = $INPUT->str('date'); 42819378907SAtari911 $eventId = $INPUT->str('eventId'); 42919378907SAtari911 430e3a9f44cSAtari911 // Find where the event actually lives 431e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 432e3a9f44cSAtari911 433e3a9f44cSAtari911 if ($storedNamespace === null) { 434e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 435e3a9f44cSAtari911 return; 436e3a9f44cSAtari911 } 437e3a9f44cSAtari911 438e3a9f44cSAtari911 // Use the found namespace 439e3a9f44cSAtari911 $namespace = $storedNamespace; 440e3a9f44cSAtari911 44119378907SAtari911 list($year, $month, $day) = explode('-', $date); 44219378907SAtari911 44319378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 44419378907SAtari911 if ($namespace) { 44519378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 44619378907SAtari911 } 44719378907SAtari911 $dataDir .= 'calendar/'; 44819378907SAtari911 44919378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 45019378907SAtari911 45119378907SAtari911 if (file_exists($eventFile)) { 45219378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 45319378907SAtari911 45419378907SAtari911 if (isset($events[$date])) { 45519378907SAtari911 foreach ($events[$date] as $event) { 45619378907SAtari911 if ($event['id'] === $eventId) { 4571d05cddcSAtari911 // Include the namespace so JavaScript knows where this event actually lives 4581d05cddcSAtari911 $event['namespace'] = $namespace; 45919378907SAtari911 echo json_encode(['success' => true, 'event' => $event]); 46019378907SAtari911 return; 46119378907SAtari911 } 46219378907SAtari911 } 46319378907SAtari911 } 46419378907SAtari911 } 46519378907SAtari911 46619378907SAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 46719378907SAtari911 } 46819378907SAtari911 46919378907SAtari911 private function loadMonth() { 47019378907SAtari911 global $INPUT; 47119378907SAtari911 472e3a9f44cSAtari911 // Prevent caching of AJAX responses 473e3a9f44cSAtari911 header('Cache-Control: no-cache, no-store, must-revalidate'); 474e3a9f44cSAtari911 header('Pragma: no-cache'); 475e3a9f44cSAtari911 header('Expires: 0'); 476e3a9f44cSAtari911 47719378907SAtari911 $namespace = $INPUT->str('namespace', ''); 47819378907SAtari911 $year = $INPUT->int('year'); 47919378907SAtari911 $month = $INPUT->int('month'); 48019378907SAtari911 481e3a9f44cSAtari911 error_log("=== Calendar loadMonth DEBUG ==="); 482e3a9f44cSAtari911 error_log("Requested: year=$year, month=$month, namespace='$namespace'"); 483e3a9f44cSAtari911 484e3a9f44cSAtari911 // Check if multi-namespace or wildcard 485e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 486e3a9f44cSAtari911 487e3a9f44cSAtari911 error_log("isMultiNamespace: " . ($isMultiNamespace ? 'true' : 'false')); 488e3a9f44cSAtari911 489e3a9f44cSAtari911 if ($isMultiNamespace) { 490e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 491e3a9f44cSAtari911 } else { 492e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 493e3a9f44cSAtari911 } 494e3a9f44cSAtari911 495e3a9f44cSAtari911 error_log("Returning " . count($events) . " date keys"); 496e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 497e3a9f44cSAtari911 error_log(" dateKey=$dateKey has " . count($dayEvents) . " events"); 498e3a9f44cSAtari911 } 499e3a9f44cSAtari911 500e3a9f44cSAtari911 echo json_encode([ 501e3a9f44cSAtari911 'success' => true, 502e3a9f44cSAtari911 'year' => $year, 503e3a9f44cSAtari911 'month' => $month, 504e3a9f44cSAtari911 'events' => $events 505e3a9f44cSAtari911 ]); 506e3a9f44cSAtari911 } 507e3a9f44cSAtari911 508e3a9f44cSAtari911 private function loadEventsSingleNamespace($namespace, $year, $month) { 50919378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 51019378907SAtari911 if ($namespace) { 51119378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 51219378907SAtari911 } 51319378907SAtari911 $dataDir .= 'calendar/'; 51419378907SAtari911 515e3a9f44cSAtari911 // Load ONLY current month 51687ac9bf3SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 51719378907SAtari911 $events = []; 51819378907SAtari911 if (file_exists($eventFile)) { 51987ac9bf3SAtari911 $contents = file_get_contents($eventFile); 52087ac9bf3SAtari911 $decoded = json_decode($contents, true); 52187ac9bf3SAtari911 if (json_last_error() === JSON_ERROR_NONE) { 52287ac9bf3SAtari911 $events = $decoded; 52387ac9bf3SAtari911 } 52487ac9bf3SAtari911 } 52587ac9bf3SAtari911 526e3a9f44cSAtari911 return $events; 52787ac9bf3SAtari911 } 528e3a9f44cSAtari911 529e3a9f44cSAtari911 private function loadEventsMultiNamespace($namespaces, $year, $month) { 530e3a9f44cSAtari911 // Check for wildcard pattern 531e3a9f44cSAtari911 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 532e3a9f44cSAtari911 $baseNamespace = $matches[1]; 533e3a9f44cSAtari911 return $this->loadEventsWildcard($baseNamespace, $year, $month); 534e3a9f44cSAtari911 } 535e3a9f44cSAtari911 536e3a9f44cSAtari911 // Check for root wildcard 537e3a9f44cSAtari911 if ($namespaces === '*') { 538e3a9f44cSAtari911 return $this->loadEventsWildcard('', $year, $month); 539e3a9f44cSAtari911 } 540e3a9f44cSAtari911 541e3a9f44cSAtari911 // Parse namespace list (semicolon separated) 542e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespaces)); 543e3a9f44cSAtari911 544e3a9f44cSAtari911 // Load events from all namespaces 545e3a9f44cSAtari911 $allEvents = []; 546e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 547e3a9f44cSAtari911 $ns = trim($ns); 548e3a9f44cSAtari911 if (empty($ns)) continue; 549e3a9f44cSAtari911 550e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($ns, $year, $month); 551e3a9f44cSAtari911 552e3a9f44cSAtari911 // Add namespace tag to each event 553e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 554e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 555e3a9f44cSAtari911 $allEvents[$dateKey] = []; 556e3a9f44cSAtari911 } 557e3a9f44cSAtari911 foreach ($dayEvents as $event) { 558e3a9f44cSAtari911 $event['_namespace'] = $ns; 559e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 560e3a9f44cSAtari911 } 56187ac9bf3SAtari911 } 56287ac9bf3SAtari911 } 56387ac9bf3SAtari911 564e3a9f44cSAtari911 return $allEvents; 565e3a9f44cSAtari911 } 56619378907SAtari911 567e3a9f44cSAtari911 private function loadEventsWildcard($baseNamespace, $year, $month) { 568e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 569e3a9f44cSAtari911 if ($baseNamespace) { 570e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 571e3a9f44cSAtari911 } 572e3a9f44cSAtari911 573e3a9f44cSAtari911 $allEvents = []; 574e3a9f44cSAtari911 575e3a9f44cSAtari911 // First, load events from the base namespace itself 576e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($baseNamespace, $year, $month); 577e3a9f44cSAtari911 578e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 579e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 580e3a9f44cSAtari911 $allEvents[$dateKey] = []; 581e3a9f44cSAtari911 } 582e3a9f44cSAtari911 foreach ($dayEvents as $event) { 583e3a9f44cSAtari911 $event['_namespace'] = $baseNamespace; 584e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 585e3a9f44cSAtari911 } 586e3a9f44cSAtari911 } 587e3a9f44cSAtari911 588e3a9f44cSAtari911 // Recursively find all subdirectories 589e3a9f44cSAtari911 $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents); 590e3a9f44cSAtari911 591e3a9f44cSAtari911 return $allEvents; 592e3a9f44cSAtari911 } 593e3a9f44cSAtari911 594e3a9f44cSAtari911 private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) { 595e3a9f44cSAtari911 if (!is_dir($dir)) return; 596e3a9f44cSAtari911 597e3a9f44cSAtari911 $items = scandir($dir); 598e3a9f44cSAtari911 foreach ($items as $item) { 599e3a9f44cSAtari911 if ($item === '.' || $item === '..') continue; 600e3a9f44cSAtari911 601e3a9f44cSAtari911 $path = $dir . $item; 602e3a9f44cSAtari911 if (is_dir($path) && $item !== 'calendar') { 603e3a9f44cSAtari911 // This is a namespace directory 604e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 605e3a9f44cSAtari911 606e3a9f44cSAtari911 // Load events from this namespace 607e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 608e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 609e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 610e3a9f44cSAtari911 $allEvents[$dateKey] = []; 611e3a9f44cSAtari911 } 612e3a9f44cSAtari911 foreach ($dayEvents as $event) { 613e3a9f44cSAtari911 $event['_namespace'] = $namespace; 614e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 615e3a9f44cSAtari911 } 616e3a9f44cSAtari911 } 617e3a9f44cSAtari911 618e3a9f44cSAtari911 // Recurse into subdirectories 619e3a9f44cSAtari911 $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents); 620e3a9f44cSAtari911 } 621e3a9f44cSAtari911 } 62219378907SAtari911 } 62319378907SAtari911 62419378907SAtari911 private function toggleTaskComplete() { 62519378907SAtari911 global $INPUT; 62619378907SAtari911 62719378907SAtari911 $namespace = $INPUT->str('namespace', ''); 62819378907SAtari911 $date = $INPUT->str('date'); 62919378907SAtari911 $eventId = $INPUT->str('eventId'); 63019378907SAtari911 $completed = $INPUT->bool('completed', false); 63119378907SAtari911 632e3a9f44cSAtari911 // Find where the event actually lives 633e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 634e3a9f44cSAtari911 635e3a9f44cSAtari911 if ($storedNamespace === null) { 636e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 637e3a9f44cSAtari911 return; 638e3a9f44cSAtari911 } 639e3a9f44cSAtari911 640e3a9f44cSAtari911 // Use the found namespace 641e3a9f44cSAtari911 $namespace = $storedNamespace; 642e3a9f44cSAtari911 64319378907SAtari911 list($year, $month, $day) = explode('-', $date); 64419378907SAtari911 64519378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 64619378907SAtari911 if ($namespace) { 64719378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 64819378907SAtari911 } 64919378907SAtari911 $dataDir .= 'calendar/'; 65019378907SAtari911 65119378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 65219378907SAtari911 65319378907SAtari911 if (file_exists($eventFile)) { 65419378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 65519378907SAtari911 65619378907SAtari911 if (isset($events[$date])) { 65719378907SAtari911 foreach ($events[$date] as $key => $event) { 65819378907SAtari911 if ($event['id'] === $eventId) { 65919378907SAtari911 $events[$date][$key]['completed'] = $completed; 66019378907SAtari911 break; 66119378907SAtari911 } 66219378907SAtari911 } 66319378907SAtari911 66419378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 66519378907SAtari911 echo json_encode(['success' => true, 'events' => $events]); 66619378907SAtari911 return; 66719378907SAtari911 } 66819378907SAtari911 } 66919378907SAtari911 67019378907SAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 67119378907SAtari911 } 67219378907SAtari911 67387ac9bf3SAtari911 private function createRecurringEvents($namespace, $startDate, $endDate, $title, $time, 67487ac9bf3SAtari911 $description, $color, $isTask, $recurrenceType, 67587ac9bf3SAtari911 $recurrenceEnd, $baseId) { 67687ac9bf3SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 67787ac9bf3SAtari911 if ($namespace) { 67887ac9bf3SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 67987ac9bf3SAtari911 } 68087ac9bf3SAtari911 $dataDir .= 'calendar/'; 68187ac9bf3SAtari911 68287ac9bf3SAtari911 if (!is_dir($dataDir)) { 68387ac9bf3SAtari911 mkdir($dataDir, 0755, true); 68487ac9bf3SAtari911 } 68587ac9bf3SAtari911 68687ac9bf3SAtari911 // Calculate recurrence interval 68787ac9bf3SAtari911 $interval = ''; 68887ac9bf3SAtari911 switch ($recurrenceType) { 68987ac9bf3SAtari911 case 'daily': $interval = '+1 day'; break; 69087ac9bf3SAtari911 case 'weekly': $interval = '+1 week'; break; 69187ac9bf3SAtari911 case 'monthly': $interval = '+1 month'; break; 69287ac9bf3SAtari911 case 'yearly': $interval = '+1 year'; break; 69387ac9bf3SAtari911 default: $interval = '+1 week'; 69487ac9bf3SAtari911 } 69587ac9bf3SAtari911 69687ac9bf3SAtari911 // Set maximum end date if not specified (1 year from start) 69787ac9bf3SAtari911 $maxEnd = $recurrenceEnd ?: date('Y-m-d', strtotime($startDate . ' +1 year')); 69887ac9bf3SAtari911 69987ac9bf3SAtari911 // Calculate event duration for multi-day events 70087ac9bf3SAtari911 $eventDuration = 0; 70187ac9bf3SAtari911 if ($endDate && $endDate !== $startDate) { 70287ac9bf3SAtari911 $start = new DateTime($startDate); 70387ac9bf3SAtari911 $end = new DateTime($endDate); 70487ac9bf3SAtari911 $eventDuration = $start->diff($end)->days; 70587ac9bf3SAtari911 } 70687ac9bf3SAtari911 70787ac9bf3SAtari911 // Generate recurring events 70887ac9bf3SAtari911 $currentDate = new DateTime($startDate); 70987ac9bf3SAtari911 $endLimit = new DateTime($maxEnd); 71087ac9bf3SAtari911 $counter = 0; 71187ac9bf3SAtari911 $maxOccurrences = 100; // Prevent infinite loops 71287ac9bf3SAtari911 71387ac9bf3SAtari911 while ($currentDate <= $endLimit && $counter < $maxOccurrences) { 71487ac9bf3SAtari911 $dateKey = $currentDate->format('Y-m-d'); 71587ac9bf3SAtari911 list($year, $month, $day) = explode('-', $dateKey); 71687ac9bf3SAtari911 71787ac9bf3SAtari911 // Calculate end date for this occurrence if multi-day 71887ac9bf3SAtari911 $occurrenceEndDate = ''; 71987ac9bf3SAtari911 if ($eventDuration > 0) { 72087ac9bf3SAtari911 $occurrenceEnd = clone $currentDate; 72187ac9bf3SAtari911 $occurrenceEnd->modify('+' . $eventDuration . ' days'); 72287ac9bf3SAtari911 $occurrenceEndDate = $occurrenceEnd->format('Y-m-d'); 72387ac9bf3SAtari911 } 72487ac9bf3SAtari911 72587ac9bf3SAtari911 // Load month file 72687ac9bf3SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 72787ac9bf3SAtari911 $events = []; 72887ac9bf3SAtari911 if (file_exists($eventFile)) { 72987ac9bf3SAtari911 $events = json_decode(file_get_contents($eventFile), true); 73087ac9bf3SAtari911 } 73187ac9bf3SAtari911 73287ac9bf3SAtari911 if (!isset($events[$dateKey])) { 73387ac9bf3SAtari911 $events[$dateKey] = []; 73487ac9bf3SAtari911 } 73587ac9bf3SAtari911 73687ac9bf3SAtari911 // Create event for this occurrence 73787ac9bf3SAtari911 $eventData = [ 73887ac9bf3SAtari911 'id' => $baseId . '-' . $counter, 73987ac9bf3SAtari911 'title' => $title, 74087ac9bf3SAtari911 'time' => $time, 7411d05cddcSAtari911 'endTime' => $endTime, 74287ac9bf3SAtari911 'description' => $description, 74387ac9bf3SAtari911 'color' => $color, 74487ac9bf3SAtari911 'isTask' => $isTask, 74587ac9bf3SAtari911 'completed' => false, 74687ac9bf3SAtari911 'endDate' => $occurrenceEndDate, 74787ac9bf3SAtari911 'recurring' => true, 74887ac9bf3SAtari911 'recurringId' => $baseId, 7491d05cddcSAtari911 'namespace' => $namespace, // Add namespace! 75087ac9bf3SAtari911 'created' => date('Y-m-d H:i:s') 75187ac9bf3SAtari911 ]; 75287ac9bf3SAtari911 75387ac9bf3SAtari911 $events[$dateKey][] = $eventData; 75487ac9bf3SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 75587ac9bf3SAtari911 75687ac9bf3SAtari911 // Move to next occurrence 75787ac9bf3SAtari911 $currentDate->modify($interval); 75887ac9bf3SAtari911 $counter++; 75987ac9bf3SAtari911 } 76087ac9bf3SAtari911 } 76187ac9bf3SAtari911 76219378907SAtari911 public function addAssets(Doku_Event $event, $param) { 76319378907SAtari911 $event->data['link'][] = array( 76419378907SAtari911 'type' => 'text/css', 76519378907SAtari911 'rel' => 'stylesheet', 76619378907SAtari911 'href' => DOKU_BASE . 'lib/plugins/calendar/style.css' 76719378907SAtari911 ); 76819378907SAtari911 76919378907SAtari911 $event->data['script'][] = array( 77019378907SAtari911 'type' => 'text/javascript', 77119378907SAtari911 'src' => DOKU_BASE . 'lib/plugins/calendar/script.js' 77219378907SAtari911 ); 77319378907SAtari911 } 774e3a9f44cSAtari911 // Helper function to find an event's stored namespace 775e3a9f44cSAtari911 private function findEventNamespace($eventId, $date, $searchNamespace) { 776e3a9f44cSAtari911 list($year, $month, $day) = explode('-', $date); 777e3a9f44cSAtari911 778e3a9f44cSAtari911 // List of namespaces to check 779e3a9f44cSAtari911 $namespacesToCheck = ['']; 780e3a9f44cSAtari911 781e3a9f44cSAtari911 // If searchNamespace is a wildcard or multi, we need to search multiple locations 782e3a9f44cSAtari911 if (!empty($searchNamespace)) { 783e3a9f44cSAtari911 if (strpos($searchNamespace, ';') !== false) { 784e3a9f44cSAtari911 // Multi-namespace - check each one 785e3a9f44cSAtari911 $namespacesToCheck = array_map('trim', explode(';', $searchNamespace)); 786e3a9f44cSAtari911 $namespacesToCheck[] = ''; // Also check default 787e3a9f44cSAtari911 } elseif (strpos($searchNamespace, '*') !== false) { 788e3a9f44cSAtari911 // Wildcard - need to scan directories 789e3a9f44cSAtari911 $baseNs = trim(str_replace('*', '', $searchNamespace), ':'); 790e3a9f44cSAtari911 $namespacesToCheck = $this->findAllNamespaces($baseNs); 791e3a9f44cSAtari911 $namespacesToCheck[] = ''; // Also check default 792e3a9f44cSAtari911 } else { 793e3a9f44cSAtari911 // Single namespace 794e3a9f44cSAtari911 $namespacesToCheck = [$searchNamespace, '']; // Check specified and default 795e3a9f44cSAtari911 } 796e3a9f44cSAtari911 } 797e3a9f44cSAtari911 798e3a9f44cSAtari911 // Search for the event in all possible namespaces 799e3a9f44cSAtari911 foreach ($namespacesToCheck as $ns) { 800e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 801e3a9f44cSAtari911 if ($ns) { 802e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $ns) . '/'; 803e3a9f44cSAtari911 } 804e3a9f44cSAtari911 $dataDir .= 'calendar/'; 805e3a9f44cSAtari911 806e3a9f44cSAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 807e3a9f44cSAtari911 808e3a9f44cSAtari911 if (file_exists($eventFile)) { 809e3a9f44cSAtari911 $events = json_decode(file_get_contents($eventFile), true); 810e3a9f44cSAtari911 if (isset($events[$date])) { 811e3a9f44cSAtari911 foreach ($events[$date] as $evt) { 812e3a9f44cSAtari911 if ($evt['id'] === $eventId) { 813e3a9f44cSAtari911 // Found the event! Return its stored namespace 814e3a9f44cSAtari911 return isset($evt['namespace']) ? $evt['namespace'] : $ns; 815e3a9f44cSAtari911 } 816e3a9f44cSAtari911 } 817e3a9f44cSAtari911 } 818e3a9f44cSAtari911 } 819e3a9f44cSAtari911 } 820e3a9f44cSAtari911 821e3a9f44cSAtari911 return null; // Event not found 822e3a9f44cSAtari911 } 823e3a9f44cSAtari911 824e3a9f44cSAtari911 // Helper to find all namespaces under a base namespace 825e3a9f44cSAtari911 private function findAllNamespaces($baseNamespace) { 826e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 827e3a9f44cSAtari911 if ($baseNamespace) { 828e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 829e3a9f44cSAtari911 } 830e3a9f44cSAtari911 831e3a9f44cSAtari911 $namespaces = []; 832e3a9f44cSAtari911 if ($baseNamespace) { 833e3a9f44cSAtari911 $namespaces[] = $baseNamespace; 834e3a9f44cSAtari911 } 835e3a9f44cSAtari911 836e3a9f44cSAtari911 $this->scanForNamespaces($dataDir, $baseNamespace, $namespaces); 837e3a9f44cSAtari911 838e3a9f44cSAtari911 return $namespaces; 839e3a9f44cSAtari911 } 840e3a9f44cSAtari911 841e3a9f44cSAtari911 // Recursive scan for namespaces 842e3a9f44cSAtari911 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 843e3a9f44cSAtari911 if (!is_dir($dir)) return; 844e3a9f44cSAtari911 845e3a9f44cSAtari911 $items = scandir($dir); 846e3a9f44cSAtari911 foreach ($items as $item) { 847e3a9f44cSAtari911 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 848e3a9f44cSAtari911 849e3a9f44cSAtari911 $path = $dir . $item; 850e3a9f44cSAtari911 if (is_dir($path)) { 851e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 852e3a9f44cSAtari911 $namespaces[] = $namespace; 853e3a9f44cSAtari911 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 854e3a9f44cSAtari911 } 855e3a9f44cSAtari911 } 856e3a9f44cSAtari911 } 857*9ccd446eSAtari911 858*9ccd446eSAtari911 /** 859*9ccd446eSAtari911 * Delete all instances of a recurring event across all months 860*9ccd446eSAtari911 */ 861*9ccd446eSAtari911 private function deleteAllRecurringInstances($recurringId, $namespace, $dataDir) { 862*9ccd446eSAtari911 // Scan all JSON files in the calendar directory 863*9ccd446eSAtari911 $calendarFiles = glob($dataDir . '*.json'); 864*9ccd446eSAtari911 865*9ccd446eSAtari911 foreach ($calendarFiles as $file) { 866*9ccd446eSAtari911 $modified = false; 867*9ccd446eSAtari911 $events = json_decode(file_get_contents($file), true); 868*9ccd446eSAtari911 869*9ccd446eSAtari911 if (!$events) continue; 870*9ccd446eSAtari911 871*9ccd446eSAtari911 // Check each date in the file 872*9ccd446eSAtari911 foreach ($events as $date => &$dayEvents) { 873*9ccd446eSAtari911 // Filter out events with matching recurringId 874*9ccd446eSAtari911 $originalCount = count($dayEvents); 875*9ccd446eSAtari911 $dayEvents = array_values(array_filter($dayEvents, function($event) use ($recurringId) { 876*9ccd446eSAtari911 $eventRecurringId = isset($event['recurringId']) ? $event['recurringId'] : null; 877*9ccd446eSAtari911 return $eventRecurringId !== $recurringId; 878*9ccd446eSAtari911 })); 879*9ccd446eSAtari911 880*9ccd446eSAtari911 if (count($dayEvents) !== $originalCount) { 881*9ccd446eSAtari911 $modified = true; 882*9ccd446eSAtari911 } 883*9ccd446eSAtari911 884*9ccd446eSAtari911 // Remove empty dates 885*9ccd446eSAtari911 if (empty($dayEvents)) { 886*9ccd446eSAtari911 unset($events[$date]); 887*9ccd446eSAtari911 } 888*9ccd446eSAtari911 } 889*9ccd446eSAtari911 890*9ccd446eSAtari911 // Save if modified 891*9ccd446eSAtari911 if ($modified) { 892*9ccd446eSAtari911 file_put_contents($file, json_encode($events, JSON_PRETTY_PRINT)); 893*9ccd446eSAtari911 } 894*9ccd446eSAtari911 } 895*9ccd446eSAtari911 } 896*9ccd446eSAtari911 897*9ccd446eSAtari911 /** 898*9ccd446eSAtari911 * Get existing event data for preserving unchanged fields during edit 899*9ccd446eSAtari911 */ 900*9ccd446eSAtari911 private function getExistingEventData($eventId, $date, $namespace) { 901*9ccd446eSAtari911 list($year, $month, $day) = explode('-', $date); 902*9ccd446eSAtari911 903*9ccd446eSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 904*9ccd446eSAtari911 if ($namespace) { 905*9ccd446eSAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 906*9ccd446eSAtari911 } 907*9ccd446eSAtari911 $dataDir .= 'calendar/'; 908*9ccd446eSAtari911 909*9ccd446eSAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 910*9ccd446eSAtari911 911*9ccd446eSAtari911 if (!file_exists($eventFile)) { 912*9ccd446eSAtari911 return null; 913*9ccd446eSAtari911 } 914*9ccd446eSAtari911 915*9ccd446eSAtari911 $events = json_decode(file_get_contents($eventFile), true); 916*9ccd446eSAtari911 917*9ccd446eSAtari911 if (!isset($events[$date])) { 918*9ccd446eSAtari911 return null; 919*9ccd446eSAtari911 } 920*9ccd446eSAtari911 921*9ccd446eSAtari911 // Find the event by ID 922*9ccd446eSAtari911 foreach ($events[$date] as $event) { 923*9ccd446eSAtari911 if ($event['id'] === $eventId) { 924*9ccd446eSAtari911 return $event; 925*9ccd446eSAtari911 } 926*9ccd446eSAtari911 } 927*9ccd446eSAtari911 928*9ccd446eSAtari911 return null; 929*9ccd446eSAtari911 } 93019378907SAtari911} 931