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', ''); 5419378907SAtari911 $description = $INPUT->str('description', ''); 5519378907SAtari911 $color = $INPUT->str('color', '#3498db'); 5619378907SAtari911 $oldDate = $INPUT->str('oldDate', ''); // Track original date for moves 5719378907SAtari911 $isTask = $INPUT->bool('isTask', false); 5819378907SAtari911 $completed = $INPUT->bool('completed', false); 5919378907SAtari911 $endDate = $INPUT->str('endDate', ''); 6087ac9bf3SAtari911 $isRecurring = $INPUT->bool('isRecurring', false); 6187ac9bf3SAtari911 $recurrenceType = $INPUT->str('recurrenceType', 'weekly'); 6287ac9bf3SAtari911 $recurrenceEnd = $INPUT->str('recurrenceEnd', ''); 6319378907SAtari911 6419378907SAtari911 if (!$date || !$title) { 6519378907SAtari911 echo json_encode(['success' => false, 'error' => 'Missing required fields']); 6619378907SAtari911 return; 6719378907SAtari911 } 6819378907SAtari911 69*e3a9f44cSAtari911 // If editing, find the event's stored namespace first (before normalizing) 70*e3a9f44cSAtari911 $storedNamespace = ''; 71*e3a9f44cSAtari911 if ($eventId) { 72*e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 73*e3a9f44cSAtari911 } 74*e3a9f44cSAtari911 75*e3a9f44cSAtari911 // Use stored namespace if editing, otherwise normalize wildcards to empty 76*e3a9f44cSAtari911 if ($eventId && $storedNamespace !== null) { 77*e3a9f44cSAtari911 $namespace = $storedNamespace; 78*e3a9f44cSAtari911 } else { 79*e3a9f44cSAtari911 // Normalize namespace: treat wildcards and multi-namespace as empty (default) for NEW events 80*e3a9f44cSAtari911 if (!empty($namespace) && (strpos($namespace, '*') !== false || strpos($namespace, ';') !== false)) { 81*e3a9f44cSAtari911 $namespace = ''; 82*e3a9f44cSAtari911 } 83*e3a9f44cSAtari911 } 84*e3a9f44cSAtari911 8587ac9bf3SAtari911 // Generate event ID if new 8687ac9bf3SAtari911 $generatedId = $eventId ?: uniqid(); 8787ac9bf3SAtari911 8887ac9bf3SAtari911 // If recurring, generate multiple events 8987ac9bf3SAtari911 if ($isRecurring) { 9087ac9bf3SAtari911 $this->createRecurringEvents($namespace, $date, $endDate, $title, $time, $description, 9187ac9bf3SAtari911 $color, $isTask, $recurrenceType, $recurrenceEnd, $generatedId); 9287ac9bf3SAtari911 echo json_encode(['success' => true]); 9387ac9bf3SAtari911 return; 9487ac9bf3SAtari911 } 9587ac9bf3SAtari911 9619378907SAtari911 list($year, $month, $day) = explode('-', $date); 9719378907SAtari911 9819378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 9919378907SAtari911 if ($namespace) { 10019378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 10119378907SAtari911 } 10219378907SAtari911 $dataDir .= 'calendar/'; 10319378907SAtari911 10419378907SAtari911 if (!is_dir($dataDir)) { 10519378907SAtari911 mkdir($dataDir, 0755, true); 10619378907SAtari911 } 10719378907SAtari911 10819378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 10919378907SAtari911 11019378907SAtari911 $events = []; 11119378907SAtari911 if (file_exists($eventFile)) { 11219378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 11319378907SAtari911 } 11419378907SAtari911 11519378907SAtari911 // If editing and date changed, remove from old date first 11619378907SAtari911 if ($eventId && $oldDate && $oldDate !== $date) { 11719378907SAtari911 list($oldYear, $oldMonth, $oldDay) = explode('-', $oldDate); 11819378907SAtari911 $oldEventFile = $dataDir . sprintf('%04d-%02d.json', $oldYear, $oldMonth); 11919378907SAtari911 12019378907SAtari911 if (file_exists($oldEventFile)) { 12119378907SAtari911 $oldEvents = json_decode(file_get_contents($oldEventFile), true); 12219378907SAtari911 if (isset($oldEvents[$oldDate])) { 123*e3a9f44cSAtari911 $oldEvents[$oldDate] = array_values(array_filter($oldEvents[$oldDate], function($evt) use ($eventId) { 12419378907SAtari911 return $evt['id'] !== $eventId; 125*e3a9f44cSAtari911 })); 12619378907SAtari911 12719378907SAtari911 if (empty($oldEvents[$oldDate])) { 12819378907SAtari911 unset($oldEvents[$oldDate]); 12919378907SAtari911 } 13019378907SAtari911 13119378907SAtari911 file_put_contents($oldEventFile, json_encode($oldEvents, JSON_PRETTY_PRINT)); 13219378907SAtari911 } 13319378907SAtari911 } 13419378907SAtari911 } 13519378907SAtari911 13619378907SAtari911 if (!isset($events[$date])) { 13719378907SAtari911 $events[$date] = []; 138*e3a9f44cSAtari911 } elseif (!is_array($events[$date])) { 139*e3a9f44cSAtari911 // Fix corrupted data - ensure it's an array 140*e3a9f44cSAtari911 error_log("Calendar saveEvent: Fixing corrupted data at $date - was not an array"); 141*e3a9f44cSAtari911 $events[$date] = []; 14219378907SAtari911 } 14319378907SAtari911 144*e3a9f44cSAtari911 // Store the namespace with the event 14519378907SAtari911 $eventData = [ 14687ac9bf3SAtari911 'id' => $generatedId, 14719378907SAtari911 'title' => $title, 14819378907SAtari911 'time' => $time, 14919378907SAtari911 'description' => $description, 15019378907SAtari911 'color' => $color, 15119378907SAtari911 'isTask' => $isTask, 15219378907SAtari911 'completed' => $completed, 15319378907SAtari911 'endDate' => $endDate, 154*e3a9f44cSAtari911 'namespace' => $namespace, // Store namespace with event 15519378907SAtari911 'created' => date('Y-m-d H:i:s') 15619378907SAtari911 ]; 15719378907SAtari911 15819378907SAtari911 // If editing, replace existing event 15919378907SAtari911 if ($eventId) { 16019378907SAtari911 $found = false; 16119378907SAtari911 foreach ($events[$date] as $key => $evt) { 16219378907SAtari911 if ($evt['id'] === $eventId) { 16319378907SAtari911 $events[$date][$key] = $eventData; 16419378907SAtari911 $found = true; 16519378907SAtari911 break; 16619378907SAtari911 } 16719378907SAtari911 } 16819378907SAtari911 if (!$found) { 16919378907SAtari911 $events[$date][] = $eventData; 17019378907SAtari911 } 17119378907SAtari911 } else { 17219378907SAtari911 $events[$date][] = $eventData; 17319378907SAtari911 } 17419378907SAtari911 17519378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 17619378907SAtari911 177*e3a9f44cSAtari911 // If event spans multiple months, add it to the first day of each subsequent month 178*e3a9f44cSAtari911 if ($endDate && $endDate !== $date) { 179*e3a9f44cSAtari911 $startDateObj = new DateTime($date); 180*e3a9f44cSAtari911 $endDateObj = new DateTime($endDate); 181*e3a9f44cSAtari911 182*e3a9f44cSAtari911 // Get the month/year of the start date 183*e3a9f44cSAtari911 $startMonth = $startDateObj->format('Y-m'); 184*e3a9f44cSAtari911 185*e3a9f44cSAtari911 // Iterate through each month the event spans 186*e3a9f44cSAtari911 $currentDate = clone $startDateObj; 187*e3a9f44cSAtari911 $currentDate->modify('first day of next month'); // Jump to first of next month 188*e3a9f44cSAtari911 189*e3a9f44cSAtari911 while ($currentDate <= $endDateObj) { 190*e3a9f44cSAtari911 $currentMonth = $currentDate->format('Y-m'); 191*e3a9f44cSAtari911 $firstDayOfMonth = $currentDate->format('Y-m-01'); 192*e3a9f44cSAtari911 193*e3a9f44cSAtari911 list($currentYear, $currentMonthNum, $currentDay) = explode('-', $firstDayOfMonth); 194*e3a9f44cSAtari911 195*e3a9f44cSAtari911 // Get the file for this month 196*e3a9f44cSAtari911 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonthNum); 197*e3a9f44cSAtari911 198*e3a9f44cSAtari911 $currentEvents = []; 199*e3a9f44cSAtari911 if (file_exists($currentEventFile)) { 200*e3a9f44cSAtari911 $contents = file_get_contents($currentEventFile); 201*e3a9f44cSAtari911 $decoded = json_decode($contents, true); 202*e3a9f44cSAtari911 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { 203*e3a9f44cSAtari911 $currentEvents = $decoded; 204*e3a9f44cSAtari911 } else { 205*e3a9f44cSAtari911 error_log("Calendar saveEvent: JSON decode error in $currentEventFile: " . json_last_error_msg()); 206*e3a9f44cSAtari911 } 207*e3a9f44cSAtari911 } 208*e3a9f44cSAtari911 209*e3a9f44cSAtari911 // Add entry for the first day of this month 210*e3a9f44cSAtari911 if (!isset($currentEvents[$firstDayOfMonth])) { 211*e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = []; 212*e3a9f44cSAtari911 } elseif (!is_array($currentEvents[$firstDayOfMonth])) { 213*e3a9f44cSAtari911 // Fix corrupted data - ensure it's an array 214*e3a9f44cSAtari911 error_log("Calendar saveEvent: Fixing corrupted data at $firstDayOfMonth - was not an array"); 215*e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = []; 216*e3a9f44cSAtari911 } 217*e3a9f44cSAtari911 218*e3a9f44cSAtari911 // Create a copy with the original start date preserved 219*e3a9f44cSAtari911 $eventDataForMonth = $eventData; 220*e3a9f44cSAtari911 $eventDataForMonth['originalStartDate'] = $date; // Preserve the actual start date 221*e3a9f44cSAtari911 222*e3a9f44cSAtari911 // Check if event already exists (when editing) 223*e3a9f44cSAtari911 $found = false; 224*e3a9f44cSAtari911 if ($eventId) { 225*e3a9f44cSAtari911 foreach ($currentEvents[$firstDayOfMonth] as $key => $evt) { 226*e3a9f44cSAtari911 if ($evt['id'] === $eventId) { 227*e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth][$key] = $eventDataForMonth; 228*e3a9f44cSAtari911 $found = true; 229*e3a9f44cSAtari911 break; 230*e3a9f44cSAtari911 } 231*e3a9f44cSAtari911 } 232*e3a9f44cSAtari911 } 233*e3a9f44cSAtari911 234*e3a9f44cSAtari911 if (!$found) { 235*e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth][] = $eventDataForMonth; 236*e3a9f44cSAtari911 } 237*e3a9f44cSAtari911 238*e3a9f44cSAtari911 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 239*e3a9f44cSAtari911 240*e3a9f44cSAtari911 // Move to next month 241*e3a9f44cSAtari911 $currentDate->modify('first day of next month'); 242*e3a9f44cSAtari911 } 243*e3a9f44cSAtari911 } 244*e3a9f44cSAtari911 24519378907SAtari911 echo json_encode(['success' => true, 'events' => $events, 'eventId' => $eventData['id']]); 24619378907SAtari911 } 24719378907SAtari911 24819378907SAtari911 private function deleteEvent() { 24919378907SAtari911 global $INPUT; 25019378907SAtari911 25119378907SAtari911 $namespace = $INPUT->str('namespace', ''); 25219378907SAtari911 $date = $INPUT->str('date'); 25319378907SAtari911 $eventId = $INPUT->str('eventId'); 25419378907SAtari911 255*e3a9f44cSAtari911 // Find where the event actually lives 256*e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 257*e3a9f44cSAtari911 258*e3a9f44cSAtari911 if ($storedNamespace === null) { 259*e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 260*e3a9f44cSAtari911 return; 261*e3a9f44cSAtari911 } 262*e3a9f44cSAtari911 263*e3a9f44cSAtari911 // Use the found namespace 264*e3a9f44cSAtari911 $namespace = $storedNamespace; 265*e3a9f44cSAtari911 26619378907SAtari911 list($year, $month, $day) = explode('-', $date); 26719378907SAtari911 26819378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 26919378907SAtari911 if ($namespace) { 27019378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 27119378907SAtari911 } 27219378907SAtari911 $dataDir .= 'calendar/'; 27319378907SAtari911 27419378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 27519378907SAtari911 276*e3a9f44cSAtari911 // First, get the event to check if it spans multiple months 277*e3a9f44cSAtari911 $eventToDelete = null; 27819378907SAtari911 if (file_exists($eventFile)) { 27919378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 28019378907SAtari911 28119378907SAtari911 if (isset($events[$date])) { 282*e3a9f44cSAtari911 foreach ($events[$date] as $event) { 283*e3a9f44cSAtari911 if ($event['id'] === $eventId) { 284*e3a9f44cSAtari911 $eventToDelete = $event; 285*e3a9f44cSAtari911 break; 286*e3a9f44cSAtari911 } 287*e3a9f44cSAtari911 } 288*e3a9f44cSAtari911 289*e3a9f44cSAtari911 $events[$date] = array_values(array_filter($events[$date], function($event) use ($eventId) { 29019378907SAtari911 return $event['id'] !== $eventId; 291*e3a9f44cSAtari911 })); 29219378907SAtari911 29319378907SAtari911 if (empty($events[$date])) { 29419378907SAtari911 unset($events[$date]); 29519378907SAtari911 } 29619378907SAtari911 29719378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 29819378907SAtari911 } 29919378907SAtari911 } 30019378907SAtari911 301*e3a9f44cSAtari911 // If event spans multiple months, delete it from the first day of each subsequent month 302*e3a9f44cSAtari911 if ($eventToDelete && isset($eventToDelete['endDate']) && $eventToDelete['endDate'] && $eventToDelete['endDate'] !== $date) { 303*e3a9f44cSAtari911 $startDateObj = new DateTime($date); 304*e3a9f44cSAtari911 $endDateObj = new DateTime($eventToDelete['endDate']); 305*e3a9f44cSAtari911 306*e3a9f44cSAtari911 // Iterate through each month the event spans 307*e3a9f44cSAtari911 $currentDate = clone $startDateObj; 308*e3a9f44cSAtari911 $currentDate->modify('first day of next month'); // Jump to first of next month 309*e3a9f44cSAtari911 310*e3a9f44cSAtari911 while ($currentDate <= $endDateObj) { 311*e3a9f44cSAtari911 $firstDayOfMonth = $currentDate->format('Y-m-01'); 312*e3a9f44cSAtari911 list($currentYear, $currentMonth, $currentDay) = explode('-', $firstDayOfMonth); 313*e3a9f44cSAtari911 314*e3a9f44cSAtari911 // Get the file for this month 315*e3a9f44cSAtari911 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonth); 316*e3a9f44cSAtari911 317*e3a9f44cSAtari911 if (file_exists($currentEventFile)) { 318*e3a9f44cSAtari911 $currentEvents = json_decode(file_get_contents($currentEventFile), true); 319*e3a9f44cSAtari911 320*e3a9f44cSAtari911 if (isset($currentEvents[$firstDayOfMonth])) { 321*e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = array_values(array_filter($currentEvents[$firstDayOfMonth], function($event) use ($eventId) { 322*e3a9f44cSAtari911 return $event['id'] !== $eventId; 323*e3a9f44cSAtari911 })); 324*e3a9f44cSAtari911 325*e3a9f44cSAtari911 if (empty($currentEvents[$firstDayOfMonth])) { 326*e3a9f44cSAtari911 unset($currentEvents[$firstDayOfMonth]); 327*e3a9f44cSAtari911 } 328*e3a9f44cSAtari911 329*e3a9f44cSAtari911 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 330*e3a9f44cSAtari911 } 331*e3a9f44cSAtari911 } 332*e3a9f44cSAtari911 333*e3a9f44cSAtari911 // Move to next month 334*e3a9f44cSAtari911 $currentDate->modify('first day of next month'); 335*e3a9f44cSAtari911 } 336*e3a9f44cSAtari911 } 337*e3a9f44cSAtari911 33819378907SAtari911 echo json_encode(['success' => true]); 33919378907SAtari911 } 34019378907SAtari911 34119378907SAtari911 private function getEvent() { 34219378907SAtari911 global $INPUT; 34319378907SAtari911 34419378907SAtari911 $namespace = $INPUT->str('namespace', ''); 34519378907SAtari911 $date = $INPUT->str('date'); 34619378907SAtari911 $eventId = $INPUT->str('eventId'); 34719378907SAtari911 348*e3a9f44cSAtari911 // Find where the event actually lives 349*e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 350*e3a9f44cSAtari911 351*e3a9f44cSAtari911 if ($storedNamespace === null) { 352*e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 353*e3a9f44cSAtari911 return; 354*e3a9f44cSAtari911 } 355*e3a9f44cSAtari911 356*e3a9f44cSAtari911 // Use the found namespace 357*e3a9f44cSAtari911 $namespace = $storedNamespace; 358*e3a9f44cSAtari911 35919378907SAtari911 list($year, $month, $day) = explode('-', $date); 36019378907SAtari911 36119378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 36219378907SAtari911 if ($namespace) { 36319378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 36419378907SAtari911 } 36519378907SAtari911 $dataDir .= 'calendar/'; 36619378907SAtari911 36719378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 36819378907SAtari911 36919378907SAtari911 if (file_exists($eventFile)) { 37019378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 37119378907SAtari911 37219378907SAtari911 if (isset($events[$date])) { 37319378907SAtari911 foreach ($events[$date] as $event) { 37419378907SAtari911 if ($event['id'] === $eventId) { 37519378907SAtari911 echo json_encode(['success' => true, 'event' => $event]); 37619378907SAtari911 return; 37719378907SAtari911 } 37819378907SAtari911 } 37919378907SAtari911 } 38019378907SAtari911 } 38119378907SAtari911 38219378907SAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 38319378907SAtari911 } 38419378907SAtari911 38519378907SAtari911 private function loadMonth() { 38619378907SAtari911 global $INPUT; 38719378907SAtari911 388*e3a9f44cSAtari911 // Prevent caching of AJAX responses 389*e3a9f44cSAtari911 header('Cache-Control: no-cache, no-store, must-revalidate'); 390*e3a9f44cSAtari911 header('Pragma: no-cache'); 391*e3a9f44cSAtari911 header('Expires: 0'); 392*e3a9f44cSAtari911 39319378907SAtari911 $namespace = $INPUT->str('namespace', ''); 39419378907SAtari911 $year = $INPUT->int('year'); 39519378907SAtari911 $month = $INPUT->int('month'); 39619378907SAtari911 397*e3a9f44cSAtari911 error_log("=== Calendar loadMonth DEBUG ==="); 398*e3a9f44cSAtari911 error_log("Requested: year=$year, month=$month, namespace='$namespace'"); 399*e3a9f44cSAtari911 400*e3a9f44cSAtari911 // Check if multi-namespace or wildcard 401*e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 402*e3a9f44cSAtari911 403*e3a9f44cSAtari911 error_log("isMultiNamespace: " . ($isMultiNamespace ? 'true' : 'false')); 404*e3a9f44cSAtari911 405*e3a9f44cSAtari911 if ($isMultiNamespace) { 406*e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 407*e3a9f44cSAtari911 } else { 408*e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 409*e3a9f44cSAtari911 } 410*e3a9f44cSAtari911 411*e3a9f44cSAtari911 error_log("Returning " . count($events) . " date keys"); 412*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 413*e3a9f44cSAtari911 error_log(" dateKey=$dateKey has " . count($dayEvents) . " events"); 414*e3a9f44cSAtari911 } 415*e3a9f44cSAtari911 416*e3a9f44cSAtari911 echo json_encode([ 417*e3a9f44cSAtari911 'success' => true, 418*e3a9f44cSAtari911 'year' => $year, 419*e3a9f44cSAtari911 'month' => $month, 420*e3a9f44cSAtari911 'events' => $events 421*e3a9f44cSAtari911 ]); 422*e3a9f44cSAtari911 } 423*e3a9f44cSAtari911 424*e3a9f44cSAtari911 private function loadEventsSingleNamespace($namespace, $year, $month) { 42519378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 42619378907SAtari911 if ($namespace) { 42719378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 42819378907SAtari911 } 42919378907SAtari911 $dataDir .= 'calendar/'; 43019378907SAtari911 431*e3a9f44cSAtari911 // Load ONLY current month 43287ac9bf3SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 43319378907SAtari911 $events = []; 43419378907SAtari911 if (file_exists($eventFile)) { 43587ac9bf3SAtari911 $contents = file_get_contents($eventFile); 43687ac9bf3SAtari911 $decoded = json_decode($contents, true); 43787ac9bf3SAtari911 if (json_last_error() === JSON_ERROR_NONE) { 43887ac9bf3SAtari911 $events = $decoded; 43987ac9bf3SAtari911 } 44087ac9bf3SAtari911 } 44187ac9bf3SAtari911 442*e3a9f44cSAtari911 return $events; 44387ac9bf3SAtari911 } 444*e3a9f44cSAtari911 445*e3a9f44cSAtari911 private function loadEventsMultiNamespace($namespaces, $year, $month) { 446*e3a9f44cSAtari911 // Check for wildcard pattern 447*e3a9f44cSAtari911 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 448*e3a9f44cSAtari911 $baseNamespace = $matches[1]; 449*e3a9f44cSAtari911 return $this->loadEventsWildcard($baseNamespace, $year, $month); 450*e3a9f44cSAtari911 } 451*e3a9f44cSAtari911 452*e3a9f44cSAtari911 // Check for root wildcard 453*e3a9f44cSAtari911 if ($namespaces === '*') { 454*e3a9f44cSAtari911 return $this->loadEventsWildcard('', $year, $month); 455*e3a9f44cSAtari911 } 456*e3a9f44cSAtari911 457*e3a9f44cSAtari911 // Parse namespace list (semicolon separated) 458*e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespaces)); 459*e3a9f44cSAtari911 460*e3a9f44cSAtari911 // Load events from all namespaces 461*e3a9f44cSAtari911 $allEvents = []; 462*e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 463*e3a9f44cSAtari911 $ns = trim($ns); 464*e3a9f44cSAtari911 if (empty($ns)) continue; 465*e3a9f44cSAtari911 466*e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($ns, $year, $month); 467*e3a9f44cSAtari911 468*e3a9f44cSAtari911 // Add namespace tag to each event 469*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 470*e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 471*e3a9f44cSAtari911 $allEvents[$dateKey] = []; 472*e3a9f44cSAtari911 } 473*e3a9f44cSAtari911 foreach ($dayEvents as $event) { 474*e3a9f44cSAtari911 $event['_namespace'] = $ns; 475*e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 476*e3a9f44cSAtari911 } 47787ac9bf3SAtari911 } 47887ac9bf3SAtari911 } 47987ac9bf3SAtari911 480*e3a9f44cSAtari911 return $allEvents; 481*e3a9f44cSAtari911 } 48219378907SAtari911 483*e3a9f44cSAtari911 private function loadEventsWildcard($baseNamespace, $year, $month) { 484*e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 485*e3a9f44cSAtari911 if ($baseNamespace) { 486*e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 487*e3a9f44cSAtari911 } 488*e3a9f44cSAtari911 489*e3a9f44cSAtari911 $allEvents = []; 490*e3a9f44cSAtari911 491*e3a9f44cSAtari911 // First, load events from the base namespace itself 492*e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($baseNamespace, $year, $month); 493*e3a9f44cSAtari911 494*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 495*e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 496*e3a9f44cSAtari911 $allEvents[$dateKey] = []; 497*e3a9f44cSAtari911 } 498*e3a9f44cSAtari911 foreach ($dayEvents as $event) { 499*e3a9f44cSAtari911 $event['_namespace'] = $baseNamespace; 500*e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 501*e3a9f44cSAtari911 } 502*e3a9f44cSAtari911 } 503*e3a9f44cSAtari911 504*e3a9f44cSAtari911 // Recursively find all subdirectories 505*e3a9f44cSAtari911 $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents); 506*e3a9f44cSAtari911 507*e3a9f44cSAtari911 return $allEvents; 508*e3a9f44cSAtari911 } 509*e3a9f44cSAtari911 510*e3a9f44cSAtari911 private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) { 511*e3a9f44cSAtari911 if (!is_dir($dir)) return; 512*e3a9f44cSAtari911 513*e3a9f44cSAtari911 $items = scandir($dir); 514*e3a9f44cSAtari911 foreach ($items as $item) { 515*e3a9f44cSAtari911 if ($item === '.' || $item === '..') continue; 516*e3a9f44cSAtari911 517*e3a9f44cSAtari911 $path = $dir . $item; 518*e3a9f44cSAtari911 if (is_dir($path) && $item !== 'calendar') { 519*e3a9f44cSAtari911 // This is a namespace directory 520*e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 521*e3a9f44cSAtari911 522*e3a9f44cSAtari911 // Load events from this namespace 523*e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 524*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 525*e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 526*e3a9f44cSAtari911 $allEvents[$dateKey] = []; 527*e3a9f44cSAtari911 } 528*e3a9f44cSAtari911 foreach ($dayEvents as $event) { 529*e3a9f44cSAtari911 $event['_namespace'] = $namespace; 530*e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 531*e3a9f44cSAtari911 } 532*e3a9f44cSAtari911 } 533*e3a9f44cSAtari911 534*e3a9f44cSAtari911 // Recurse into subdirectories 535*e3a9f44cSAtari911 $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents); 536*e3a9f44cSAtari911 } 537*e3a9f44cSAtari911 } 53819378907SAtari911 } 53919378907SAtari911 54019378907SAtari911 private function toggleTaskComplete() { 54119378907SAtari911 global $INPUT; 54219378907SAtari911 54319378907SAtari911 $namespace = $INPUT->str('namespace', ''); 54419378907SAtari911 $date = $INPUT->str('date'); 54519378907SAtari911 $eventId = $INPUT->str('eventId'); 54619378907SAtari911 $completed = $INPUT->bool('completed', false); 54719378907SAtari911 548*e3a9f44cSAtari911 // Find where the event actually lives 549*e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 550*e3a9f44cSAtari911 551*e3a9f44cSAtari911 if ($storedNamespace === null) { 552*e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 553*e3a9f44cSAtari911 return; 554*e3a9f44cSAtari911 } 555*e3a9f44cSAtari911 556*e3a9f44cSAtari911 // Use the found namespace 557*e3a9f44cSAtari911 $namespace = $storedNamespace; 558*e3a9f44cSAtari911 55919378907SAtari911 list($year, $month, $day) = explode('-', $date); 56019378907SAtari911 56119378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 56219378907SAtari911 if ($namespace) { 56319378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 56419378907SAtari911 } 56519378907SAtari911 $dataDir .= 'calendar/'; 56619378907SAtari911 56719378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 56819378907SAtari911 56919378907SAtari911 if (file_exists($eventFile)) { 57019378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 57119378907SAtari911 57219378907SAtari911 if (isset($events[$date])) { 57319378907SAtari911 foreach ($events[$date] as $key => $event) { 57419378907SAtari911 if ($event['id'] === $eventId) { 57519378907SAtari911 $events[$date][$key]['completed'] = $completed; 57619378907SAtari911 break; 57719378907SAtari911 } 57819378907SAtari911 } 57919378907SAtari911 58019378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 58119378907SAtari911 echo json_encode(['success' => true, 'events' => $events]); 58219378907SAtari911 return; 58319378907SAtari911 } 58419378907SAtari911 } 58519378907SAtari911 58619378907SAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 58719378907SAtari911 } 58819378907SAtari911 58987ac9bf3SAtari911 private function createRecurringEvents($namespace, $startDate, $endDate, $title, $time, 59087ac9bf3SAtari911 $description, $color, $isTask, $recurrenceType, 59187ac9bf3SAtari911 $recurrenceEnd, $baseId) { 59287ac9bf3SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 59387ac9bf3SAtari911 if ($namespace) { 59487ac9bf3SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 59587ac9bf3SAtari911 } 59687ac9bf3SAtari911 $dataDir .= 'calendar/'; 59787ac9bf3SAtari911 59887ac9bf3SAtari911 if (!is_dir($dataDir)) { 59987ac9bf3SAtari911 mkdir($dataDir, 0755, true); 60087ac9bf3SAtari911 } 60187ac9bf3SAtari911 60287ac9bf3SAtari911 // Calculate recurrence interval 60387ac9bf3SAtari911 $interval = ''; 60487ac9bf3SAtari911 switch ($recurrenceType) { 60587ac9bf3SAtari911 case 'daily': $interval = '+1 day'; break; 60687ac9bf3SAtari911 case 'weekly': $interval = '+1 week'; break; 60787ac9bf3SAtari911 case 'monthly': $interval = '+1 month'; break; 60887ac9bf3SAtari911 case 'yearly': $interval = '+1 year'; break; 60987ac9bf3SAtari911 default: $interval = '+1 week'; 61087ac9bf3SAtari911 } 61187ac9bf3SAtari911 61287ac9bf3SAtari911 // Set maximum end date if not specified (1 year from start) 61387ac9bf3SAtari911 $maxEnd = $recurrenceEnd ?: date('Y-m-d', strtotime($startDate . ' +1 year')); 61487ac9bf3SAtari911 61587ac9bf3SAtari911 // Calculate event duration for multi-day events 61687ac9bf3SAtari911 $eventDuration = 0; 61787ac9bf3SAtari911 if ($endDate && $endDate !== $startDate) { 61887ac9bf3SAtari911 $start = new DateTime($startDate); 61987ac9bf3SAtari911 $end = new DateTime($endDate); 62087ac9bf3SAtari911 $eventDuration = $start->diff($end)->days; 62187ac9bf3SAtari911 } 62287ac9bf3SAtari911 62387ac9bf3SAtari911 // Generate recurring events 62487ac9bf3SAtari911 $currentDate = new DateTime($startDate); 62587ac9bf3SAtari911 $endLimit = new DateTime($maxEnd); 62687ac9bf3SAtari911 $counter = 0; 62787ac9bf3SAtari911 $maxOccurrences = 100; // Prevent infinite loops 62887ac9bf3SAtari911 62987ac9bf3SAtari911 while ($currentDate <= $endLimit && $counter < $maxOccurrences) { 63087ac9bf3SAtari911 $dateKey = $currentDate->format('Y-m-d'); 63187ac9bf3SAtari911 list($year, $month, $day) = explode('-', $dateKey); 63287ac9bf3SAtari911 63387ac9bf3SAtari911 // Calculate end date for this occurrence if multi-day 63487ac9bf3SAtari911 $occurrenceEndDate = ''; 63587ac9bf3SAtari911 if ($eventDuration > 0) { 63687ac9bf3SAtari911 $occurrenceEnd = clone $currentDate; 63787ac9bf3SAtari911 $occurrenceEnd->modify('+' . $eventDuration . ' days'); 63887ac9bf3SAtari911 $occurrenceEndDate = $occurrenceEnd->format('Y-m-d'); 63987ac9bf3SAtari911 } 64087ac9bf3SAtari911 64187ac9bf3SAtari911 // Load month file 64287ac9bf3SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 64387ac9bf3SAtari911 $events = []; 64487ac9bf3SAtari911 if (file_exists($eventFile)) { 64587ac9bf3SAtari911 $events = json_decode(file_get_contents($eventFile), true); 64687ac9bf3SAtari911 } 64787ac9bf3SAtari911 64887ac9bf3SAtari911 if (!isset($events[$dateKey])) { 64987ac9bf3SAtari911 $events[$dateKey] = []; 65087ac9bf3SAtari911 } 65187ac9bf3SAtari911 65287ac9bf3SAtari911 // Create event for this occurrence 65387ac9bf3SAtari911 $eventData = [ 65487ac9bf3SAtari911 'id' => $baseId . '-' . $counter, 65587ac9bf3SAtari911 'title' => $title, 65687ac9bf3SAtari911 'time' => $time, 65787ac9bf3SAtari911 'description' => $description, 65887ac9bf3SAtari911 'color' => $color, 65987ac9bf3SAtari911 'isTask' => $isTask, 66087ac9bf3SAtari911 'completed' => false, 66187ac9bf3SAtari911 'endDate' => $occurrenceEndDate, 66287ac9bf3SAtari911 'recurring' => true, 66387ac9bf3SAtari911 'recurringId' => $baseId, 66487ac9bf3SAtari911 'created' => date('Y-m-d H:i:s') 66587ac9bf3SAtari911 ]; 66687ac9bf3SAtari911 66787ac9bf3SAtari911 $events[$dateKey][] = $eventData; 66887ac9bf3SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 66987ac9bf3SAtari911 67087ac9bf3SAtari911 // Move to next occurrence 67187ac9bf3SAtari911 $currentDate->modify($interval); 67287ac9bf3SAtari911 $counter++; 67387ac9bf3SAtari911 } 67487ac9bf3SAtari911 } 67587ac9bf3SAtari911 67619378907SAtari911 public function addAssets(Doku_Event $event, $param) { 67719378907SAtari911 $event->data['link'][] = array( 67819378907SAtari911 'type' => 'text/css', 67919378907SAtari911 'rel' => 'stylesheet', 68019378907SAtari911 'href' => DOKU_BASE . 'lib/plugins/calendar/style.css' 68119378907SAtari911 ); 68219378907SAtari911 68319378907SAtari911 $event->data['script'][] = array( 68419378907SAtari911 'type' => 'text/javascript', 68519378907SAtari911 'src' => DOKU_BASE . 'lib/plugins/calendar/script.js' 68619378907SAtari911 ); 68719378907SAtari911 } 688*e3a9f44cSAtari911 // Helper function to find an event's stored namespace 689*e3a9f44cSAtari911 private function findEventNamespace($eventId, $date, $searchNamespace) { 690*e3a9f44cSAtari911 list($year, $month, $day) = explode('-', $date); 691*e3a9f44cSAtari911 692*e3a9f44cSAtari911 // List of namespaces to check 693*e3a9f44cSAtari911 $namespacesToCheck = ['']; 694*e3a9f44cSAtari911 695*e3a9f44cSAtari911 // If searchNamespace is a wildcard or multi, we need to search multiple locations 696*e3a9f44cSAtari911 if (!empty($searchNamespace)) { 697*e3a9f44cSAtari911 if (strpos($searchNamespace, ';') !== false) { 698*e3a9f44cSAtari911 // Multi-namespace - check each one 699*e3a9f44cSAtari911 $namespacesToCheck = array_map('trim', explode(';', $searchNamespace)); 700*e3a9f44cSAtari911 $namespacesToCheck[] = ''; // Also check default 701*e3a9f44cSAtari911 } elseif (strpos($searchNamespace, '*') !== false) { 702*e3a9f44cSAtari911 // Wildcard - need to scan directories 703*e3a9f44cSAtari911 $baseNs = trim(str_replace('*', '', $searchNamespace), ':'); 704*e3a9f44cSAtari911 $namespacesToCheck = $this->findAllNamespaces($baseNs); 705*e3a9f44cSAtari911 $namespacesToCheck[] = ''; // Also check default 706*e3a9f44cSAtari911 } else { 707*e3a9f44cSAtari911 // Single namespace 708*e3a9f44cSAtari911 $namespacesToCheck = [$searchNamespace, '']; // Check specified and default 709*e3a9f44cSAtari911 } 710*e3a9f44cSAtari911 } 711*e3a9f44cSAtari911 712*e3a9f44cSAtari911 // Search for the event in all possible namespaces 713*e3a9f44cSAtari911 foreach ($namespacesToCheck as $ns) { 714*e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 715*e3a9f44cSAtari911 if ($ns) { 716*e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $ns) . '/'; 717*e3a9f44cSAtari911 } 718*e3a9f44cSAtari911 $dataDir .= 'calendar/'; 719*e3a9f44cSAtari911 720*e3a9f44cSAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 721*e3a9f44cSAtari911 722*e3a9f44cSAtari911 if (file_exists($eventFile)) { 723*e3a9f44cSAtari911 $events = json_decode(file_get_contents($eventFile), true); 724*e3a9f44cSAtari911 if (isset($events[$date])) { 725*e3a9f44cSAtari911 foreach ($events[$date] as $evt) { 726*e3a9f44cSAtari911 if ($evt['id'] === $eventId) { 727*e3a9f44cSAtari911 // Found the event! Return its stored namespace 728*e3a9f44cSAtari911 return isset($evt['namespace']) ? $evt['namespace'] : $ns; 729*e3a9f44cSAtari911 } 730*e3a9f44cSAtari911 } 731*e3a9f44cSAtari911 } 732*e3a9f44cSAtari911 } 733*e3a9f44cSAtari911 } 734*e3a9f44cSAtari911 735*e3a9f44cSAtari911 return null; // Event not found 736*e3a9f44cSAtari911 } 737*e3a9f44cSAtari911 738*e3a9f44cSAtari911 // Helper to find all namespaces under a base namespace 739*e3a9f44cSAtari911 private function findAllNamespaces($baseNamespace) { 740*e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 741*e3a9f44cSAtari911 if ($baseNamespace) { 742*e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 743*e3a9f44cSAtari911 } 744*e3a9f44cSAtari911 745*e3a9f44cSAtari911 $namespaces = []; 746*e3a9f44cSAtari911 if ($baseNamespace) { 747*e3a9f44cSAtari911 $namespaces[] = $baseNamespace; 748*e3a9f44cSAtari911 } 749*e3a9f44cSAtari911 750*e3a9f44cSAtari911 $this->scanForNamespaces($dataDir, $baseNamespace, $namespaces); 751*e3a9f44cSAtari911 752*e3a9f44cSAtari911 return $namespaces; 753*e3a9f44cSAtari911 } 754*e3a9f44cSAtari911 755*e3a9f44cSAtari911 // Recursive scan for namespaces 756*e3a9f44cSAtari911 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 757*e3a9f44cSAtari911 if (!is_dir($dir)) return; 758*e3a9f44cSAtari911 759*e3a9f44cSAtari911 $items = scandir($dir); 760*e3a9f44cSAtari911 foreach ($items as $item) { 761*e3a9f44cSAtari911 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 762*e3a9f44cSAtari911 763*e3a9f44cSAtari911 $path = $dir . $item; 764*e3a9f44cSAtari911 if (is_dir($path)) { 765*e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 766*e3a9f44cSAtari911 $namespaces[] = $namespace; 767*e3a9f44cSAtari911 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 768*e3a9f44cSAtari911 } 769*e3a9f44cSAtari911 } 770*e3a9f44cSAtari911 } 77119378907SAtari911} 772