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', ''); 54*1d05cddcSAtari911 $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 70*1d05cddcSAtari911 // If editing, find the event's stored namespace (for finding/deleting old event) 71e3a9f44cSAtari911 $storedNamespace = ''; 72*1d05cddcSAtari911 $oldNamespace = ''; 73e3a9f44cSAtari911 if ($eventId) { 74*1d05cddcSAtari911 // Use oldDate if available (date was changed), otherwise use current date 75*1d05cddcSAtari911 $searchDate = ($oldDate && $oldDate !== $date) ? $oldDate : $date; 76*1d05cddcSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $searchDate, $namespace); 77*1d05cddcSAtari911 78*1d05cddcSAtari911 // Store the old namespace for deletion purposes 79*1d05cddcSAtari911 if ($storedNamespace !== null) { 80*1d05cddcSAtari911 $oldNamespace = $storedNamespace; 81*1d05cddcSAtari911 error_log("Calendar saveEvent: Found existing event in namespace '$oldNamespace'"); 82*1d05cddcSAtari911 } 83e3a9f44cSAtari911 } 84e3a9f44cSAtari911 85*1d05cddcSAtari911 // Use the namespace provided by the user (allow namespace changes!) 86*1d05cddcSAtari911 // But normalize wildcards and multi-namespace to empty for NEW events 87*1d05cddcSAtari911 if (!$eventId) { 88*1d05cddcSAtari911 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)) { 91*1d05cddcSAtari911 error_log("Calendar saveEvent: Namespace contains wildcard/multi, clearing to empty"); 92e3a9f44cSAtari911 $namespace = ''; 93*1d05cddcSAtari911 } else { 94*1d05cddcSAtari911 error_log("Calendar saveEvent: Namespace is clean, keeping as '$namespace'"); 95e3a9f44cSAtari911 } 96*1d05cddcSAtari911 } else { 97*1d05cddcSAtari911 error_log("Calendar saveEvent: EDITING event $eventId, user selected namespace='$namespace'"); 98e3a9f44cSAtari911 } 99e3a9f44cSAtari911 10087ac9bf3SAtari911 // Generate event ID if new 10187ac9bf3SAtari911 $generatedId = $eventId ?: uniqid(); 10287ac9bf3SAtari911 10387ac9bf3SAtari911 // If recurring, generate multiple events 10487ac9bf3SAtari911 if ($isRecurring) { 10587ac9bf3SAtari911 $this->createRecurringEvents($namespace, $date, $endDate, $title, $time, $description, 10687ac9bf3SAtari911 $color, $isTask, $recurrenceType, $recurrenceEnd, $generatedId); 10787ac9bf3SAtari911 echo json_encode(['success' => true]); 10887ac9bf3SAtari911 return; 10987ac9bf3SAtari911 } 11087ac9bf3SAtari911 11119378907SAtari911 list($year, $month, $day) = explode('-', $date); 11219378907SAtari911 113*1d05cddcSAtari911 // NEW namespace directory (where we'll save) 11419378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 11519378907SAtari911 if ($namespace) { 11619378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 11719378907SAtari911 } 11819378907SAtari911 $dataDir .= 'calendar/'; 11919378907SAtari911 12019378907SAtari911 if (!is_dir($dataDir)) { 12119378907SAtari911 mkdir($dataDir, 0755, true); 12219378907SAtari911 } 12319378907SAtari911 12419378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 12519378907SAtari911 12619378907SAtari911 $events = []; 12719378907SAtari911 if (file_exists($eventFile)) { 12819378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 12919378907SAtari911 } 13019378907SAtari911 131*1d05cddcSAtari911 // If editing and (date changed OR namespace changed), remove from old location first 132*1d05cddcSAtari911 $namespaceChanged = ($eventId && $oldNamespace !== '' && $oldNamespace !== $namespace); 133*1d05cddcSAtari911 $dateChanged = ($eventId && $oldDate && $oldDate !== $date); 134*1d05cddcSAtari911 135*1d05cddcSAtari911 if ($namespaceChanged || $dateChanged) { 136*1d05cddcSAtari911 // Construct OLD data directory using OLD namespace 137*1d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/'; 138*1d05cddcSAtari911 if ($oldNamespace) { 139*1d05cddcSAtari911 $oldDataDir .= str_replace(':', '/', $oldNamespace) . '/'; 140*1d05cddcSAtari911 } 141*1d05cddcSAtari911 $oldDataDir .= 'calendar/'; 142*1d05cddcSAtari911 143*1d05cddcSAtari911 $deleteDate = $dateChanged ? $oldDate : $date; 144*1d05cddcSAtari911 list($oldYear, $oldMonth, $oldDay) = explode('-', $deleteDate); 145*1d05cddcSAtari911 $oldEventFile = $oldDataDir . sprintf('%04d-%02d.json', $oldYear, $oldMonth); 14619378907SAtari911 14719378907SAtari911 if (file_exists($oldEventFile)) { 14819378907SAtari911 $oldEvents = json_decode(file_get_contents($oldEventFile), true); 149*1d05cddcSAtari911 if (isset($oldEvents[$deleteDate])) { 150*1d05cddcSAtari911 $oldEvents[$deleteDate] = array_values(array_filter($oldEvents[$deleteDate], function($evt) use ($eventId) { 15119378907SAtari911 return $evt['id'] !== $eventId; 152e3a9f44cSAtari911 })); 15319378907SAtari911 154*1d05cddcSAtari911 if (empty($oldEvents[$deleteDate])) { 155*1d05cddcSAtari911 unset($oldEvents[$deleteDate]); 15619378907SAtari911 } 15719378907SAtari911 15819378907SAtari911 file_put_contents($oldEventFile, json_encode($oldEvents, JSON_PRETTY_PRINT)); 159*1d05cddcSAtari911 error_log("Calendar saveEvent: Deleted event from old location - namespace:'$oldNamespace', date:'$deleteDate'"); 16019378907SAtari911 } 16119378907SAtari911 } 16219378907SAtari911 } 16319378907SAtari911 16419378907SAtari911 if (!isset($events[$date])) { 16519378907SAtari911 $events[$date] = []; 166e3a9f44cSAtari911 } elseif (!is_array($events[$date])) { 167e3a9f44cSAtari911 // Fix corrupted data - ensure it's an array 168e3a9f44cSAtari911 error_log("Calendar saveEvent: Fixing corrupted data at $date - was not an array"); 169e3a9f44cSAtari911 $events[$date] = []; 17019378907SAtari911 } 17119378907SAtari911 172e3a9f44cSAtari911 // Store the namespace with the event 17319378907SAtari911 $eventData = [ 17487ac9bf3SAtari911 'id' => $generatedId, 17519378907SAtari911 'title' => $title, 17619378907SAtari911 'time' => $time, 177*1d05cddcSAtari911 'endTime' => $endTime, 17819378907SAtari911 'description' => $description, 17919378907SAtari911 'color' => $color, 18019378907SAtari911 'isTask' => $isTask, 18119378907SAtari911 'completed' => $completed, 18219378907SAtari911 'endDate' => $endDate, 183e3a9f44cSAtari911 'namespace' => $namespace, // Store namespace with event 18419378907SAtari911 'created' => date('Y-m-d H:i:s') 18519378907SAtari911 ]; 18619378907SAtari911 187*1d05cddcSAtari911 // Debug logging 188*1d05cddcSAtari911 error_log("Calendar saveEvent: Saving event '$title' with namespace='$namespace' to file $eventFile"); 189*1d05cddcSAtari911 19019378907SAtari911 // If editing, replace existing event 19119378907SAtari911 if ($eventId) { 19219378907SAtari911 $found = false; 19319378907SAtari911 foreach ($events[$date] as $key => $evt) { 19419378907SAtari911 if ($evt['id'] === $eventId) { 19519378907SAtari911 $events[$date][$key] = $eventData; 19619378907SAtari911 $found = true; 19719378907SAtari911 break; 19819378907SAtari911 } 19919378907SAtari911 } 20019378907SAtari911 if (!$found) { 20119378907SAtari911 $events[$date][] = $eventData; 20219378907SAtari911 } 20319378907SAtari911 } else { 20419378907SAtari911 $events[$date][] = $eventData; 20519378907SAtari911 } 20619378907SAtari911 20719378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 20819378907SAtari911 209e3a9f44cSAtari911 // If event spans multiple months, add it to the first day of each subsequent month 210e3a9f44cSAtari911 if ($endDate && $endDate !== $date) { 211e3a9f44cSAtari911 $startDateObj = new DateTime($date); 212e3a9f44cSAtari911 $endDateObj = new DateTime($endDate); 213e3a9f44cSAtari911 214e3a9f44cSAtari911 // Get the month/year of the start date 215e3a9f44cSAtari911 $startMonth = $startDateObj->format('Y-m'); 216e3a9f44cSAtari911 217e3a9f44cSAtari911 // Iterate through each month the event spans 218e3a9f44cSAtari911 $currentDate = clone $startDateObj; 219e3a9f44cSAtari911 $currentDate->modify('first day of next month'); // Jump to first of next month 220e3a9f44cSAtari911 221e3a9f44cSAtari911 while ($currentDate <= $endDateObj) { 222e3a9f44cSAtari911 $currentMonth = $currentDate->format('Y-m'); 223e3a9f44cSAtari911 $firstDayOfMonth = $currentDate->format('Y-m-01'); 224e3a9f44cSAtari911 225e3a9f44cSAtari911 list($currentYear, $currentMonthNum, $currentDay) = explode('-', $firstDayOfMonth); 226e3a9f44cSAtari911 227e3a9f44cSAtari911 // Get the file for this month 228e3a9f44cSAtari911 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonthNum); 229e3a9f44cSAtari911 230e3a9f44cSAtari911 $currentEvents = []; 231e3a9f44cSAtari911 if (file_exists($currentEventFile)) { 232e3a9f44cSAtari911 $contents = file_get_contents($currentEventFile); 233e3a9f44cSAtari911 $decoded = json_decode($contents, true); 234e3a9f44cSAtari911 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { 235e3a9f44cSAtari911 $currentEvents = $decoded; 236e3a9f44cSAtari911 } else { 237e3a9f44cSAtari911 error_log("Calendar saveEvent: JSON decode error in $currentEventFile: " . json_last_error_msg()); 238e3a9f44cSAtari911 } 239e3a9f44cSAtari911 } 240e3a9f44cSAtari911 241e3a9f44cSAtari911 // Add entry for the first day of this month 242e3a9f44cSAtari911 if (!isset($currentEvents[$firstDayOfMonth])) { 243e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = []; 244e3a9f44cSAtari911 } elseif (!is_array($currentEvents[$firstDayOfMonth])) { 245e3a9f44cSAtari911 // Fix corrupted data - ensure it's an array 246e3a9f44cSAtari911 error_log("Calendar saveEvent: Fixing corrupted data at $firstDayOfMonth - was not an array"); 247e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = []; 248e3a9f44cSAtari911 } 249e3a9f44cSAtari911 250e3a9f44cSAtari911 // Create a copy with the original start date preserved 251e3a9f44cSAtari911 $eventDataForMonth = $eventData; 252e3a9f44cSAtari911 $eventDataForMonth['originalStartDate'] = $date; // Preserve the actual start date 253e3a9f44cSAtari911 254e3a9f44cSAtari911 // Check if event already exists (when editing) 255e3a9f44cSAtari911 $found = false; 256e3a9f44cSAtari911 if ($eventId) { 257e3a9f44cSAtari911 foreach ($currentEvents[$firstDayOfMonth] as $key => $evt) { 258e3a9f44cSAtari911 if ($evt['id'] === $eventId) { 259e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth][$key] = $eventDataForMonth; 260e3a9f44cSAtari911 $found = true; 261e3a9f44cSAtari911 break; 262e3a9f44cSAtari911 } 263e3a9f44cSAtari911 } 264e3a9f44cSAtari911 } 265e3a9f44cSAtari911 266e3a9f44cSAtari911 if (!$found) { 267e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth][] = $eventDataForMonth; 268e3a9f44cSAtari911 } 269e3a9f44cSAtari911 270e3a9f44cSAtari911 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 271e3a9f44cSAtari911 272e3a9f44cSAtari911 // Move to next month 273e3a9f44cSAtari911 $currentDate->modify('first day of next month'); 274e3a9f44cSAtari911 } 275e3a9f44cSAtari911 } 276e3a9f44cSAtari911 27719378907SAtari911 echo json_encode(['success' => true, 'events' => $events, 'eventId' => $eventData['id']]); 27819378907SAtari911 } 27919378907SAtari911 28019378907SAtari911 private function deleteEvent() { 28119378907SAtari911 global $INPUT; 28219378907SAtari911 28319378907SAtari911 $namespace = $INPUT->str('namespace', ''); 28419378907SAtari911 $date = $INPUT->str('date'); 28519378907SAtari911 $eventId = $INPUT->str('eventId'); 28619378907SAtari911 287e3a9f44cSAtari911 // Find where the event actually lives 288e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 289e3a9f44cSAtari911 290e3a9f44cSAtari911 if ($storedNamespace === null) { 291e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 292e3a9f44cSAtari911 return; 293e3a9f44cSAtari911 } 294e3a9f44cSAtari911 295e3a9f44cSAtari911 // Use the found namespace 296e3a9f44cSAtari911 $namespace = $storedNamespace; 297e3a9f44cSAtari911 29819378907SAtari911 list($year, $month, $day) = explode('-', $date); 29919378907SAtari911 30019378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 30119378907SAtari911 if ($namespace) { 30219378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 30319378907SAtari911 } 30419378907SAtari911 $dataDir .= 'calendar/'; 30519378907SAtari911 30619378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 30719378907SAtari911 308e3a9f44cSAtari911 // First, get the event to check if it spans multiple months 309e3a9f44cSAtari911 $eventToDelete = null; 31019378907SAtari911 if (file_exists($eventFile)) { 31119378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 31219378907SAtari911 31319378907SAtari911 if (isset($events[$date])) { 314e3a9f44cSAtari911 foreach ($events[$date] as $event) { 315e3a9f44cSAtari911 if ($event['id'] === $eventId) { 316e3a9f44cSAtari911 $eventToDelete = $event; 317e3a9f44cSAtari911 break; 318e3a9f44cSAtari911 } 319e3a9f44cSAtari911 } 320e3a9f44cSAtari911 321e3a9f44cSAtari911 $events[$date] = array_values(array_filter($events[$date], function($event) use ($eventId) { 32219378907SAtari911 return $event['id'] !== $eventId; 323e3a9f44cSAtari911 })); 32419378907SAtari911 32519378907SAtari911 if (empty($events[$date])) { 32619378907SAtari911 unset($events[$date]); 32719378907SAtari911 } 32819378907SAtari911 32919378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 33019378907SAtari911 } 33119378907SAtari911 } 33219378907SAtari911 333e3a9f44cSAtari911 // If event spans multiple months, delete it from the first day of each subsequent month 334e3a9f44cSAtari911 if ($eventToDelete && isset($eventToDelete['endDate']) && $eventToDelete['endDate'] && $eventToDelete['endDate'] !== $date) { 335e3a9f44cSAtari911 $startDateObj = new DateTime($date); 336e3a9f44cSAtari911 $endDateObj = new DateTime($eventToDelete['endDate']); 337e3a9f44cSAtari911 338e3a9f44cSAtari911 // Iterate through each month the event spans 339e3a9f44cSAtari911 $currentDate = clone $startDateObj; 340e3a9f44cSAtari911 $currentDate->modify('first day of next month'); // Jump to first of next month 341e3a9f44cSAtari911 342e3a9f44cSAtari911 while ($currentDate <= $endDateObj) { 343e3a9f44cSAtari911 $firstDayOfMonth = $currentDate->format('Y-m-01'); 344e3a9f44cSAtari911 list($currentYear, $currentMonth, $currentDay) = explode('-', $firstDayOfMonth); 345e3a9f44cSAtari911 346e3a9f44cSAtari911 // Get the file for this month 347e3a9f44cSAtari911 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonth); 348e3a9f44cSAtari911 349e3a9f44cSAtari911 if (file_exists($currentEventFile)) { 350e3a9f44cSAtari911 $currentEvents = json_decode(file_get_contents($currentEventFile), true); 351e3a9f44cSAtari911 352e3a9f44cSAtari911 if (isset($currentEvents[$firstDayOfMonth])) { 353e3a9f44cSAtari911 $currentEvents[$firstDayOfMonth] = array_values(array_filter($currentEvents[$firstDayOfMonth], function($event) use ($eventId) { 354e3a9f44cSAtari911 return $event['id'] !== $eventId; 355e3a9f44cSAtari911 })); 356e3a9f44cSAtari911 357e3a9f44cSAtari911 if (empty($currentEvents[$firstDayOfMonth])) { 358e3a9f44cSAtari911 unset($currentEvents[$firstDayOfMonth]); 359e3a9f44cSAtari911 } 360e3a9f44cSAtari911 361e3a9f44cSAtari911 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 362e3a9f44cSAtari911 } 363e3a9f44cSAtari911 } 364e3a9f44cSAtari911 365e3a9f44cSAtari911 // Move to next month 366e3a9f44cSAtari911 $currentDate->modify('first day of next month'); 367e3a9f44cSAtari911 } 368e3a9f44cSAtari911 } 369e3a9f44cSAtari911 37019378907SAtari911 echo json_encode(['success' => true]); 37119378907SAtari911 } 37219378907SAtari911 37319378907SAtari911 private function getEvent() { 37419378907SAtari911 global $INPUT; 37519378907SAtari911 37619378907SAtari911 $namespace = $INPUT->str('namespace', ''); 37719378907SAtari911 $date = $INPUT->str('date'); 37819378907SAtari911 $eventId = $INPUT->str('eventId'); 37919378907SAtari911 380e3a9f44cSAtari911 // Find where the event actually lives 381e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 382e3a9f44cSAtari911 383e3a9f44cSAtari911 if ($storedNamespace === null) { 384e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 385e3a9f44cSAtari911 return; 386e3a9f44cSAtari911 } 387e3a9f44cSAtari911 388e3a9f44cSAtari911 // Use the found namespace 389e3a9f44cSAtari911 $namespace = $storedNamespace; 390e3a9f44cSAtari911 39119378907SAtari911 list($year, $month, $day) = explode('-', $date); 39219378907SAtari911 39319378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 39419378907SAtari911 if ($namespace) { 39519378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 39619378907SAtari911 } 39719378907SAtari911 $dataDir .= 'calendar/'; 39819378907SAtari911 39919378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 40019378907SAtari911 40119378907SAtari911 if (file_exists($eventFile)) { 40219378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 40319378907SAtari911 40419378907SAtari911 if (isset($events[$date])) { 40519378907SAtari911 foreach ($events[$date] as $event) { 40619378907SAtari911 if ($event['id'] === $eventId) { 407*1d05cddcSAtari911 // Include the namespace so JavaScript knows where this event actually lives 408*1d05cddcSAtari911 $event['namespace'] = $namespace; 40919378907SAtari911 echo json_encode(['success' => true, 'event' => $event]); 41019378907SAtari911 return; 41119378907SAtari911 } 41219378907SAtari911 } 41319378907SAtari911 } 41419378907SAtari911 } 41519378907SAtari911 41619378907SAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 41719378907SAtari911 } 41819378907SAtari911 41919378907SAtari911 private function loadMonth() { 42019378907SAtari911 global $INPUT; 42119378907SAtari911 422e3a9f44cSAtari911 // Prevent caching of AJAX responses 423e3a9f44cSAtari911 header('Cache-Control: no-cache, no-store, must-revalidate'); 424e3a9f44cSAtari911 header('Pragma: no-cache'); 425e3a9f44cSAtari911 header('Expires: 0'); 426e3a9f44cSAtari911 42719378907SAtari911 $namespace = $INPUT->str('namespace', ''); 42819378907SAtari911 $year = $INPUT->int('year'); 42919378907SAtari911 $month = $INPUT->int('month'); 43019378907SAtari911 431e3a9f44cSAtari911 error_log("=== Calendar loadMonth DEBUG ==="); 432e3a9f44cSAtari911 error_log("Requested: year=$year, month=$month, namespace='$namespace'"); 433e3a9f44cSAtari911 434e3a9f44cSAtari911 // Check if multi-namespace or wildcard 435e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 436e3a9f44cSAtari911 437e3a9f44cSAtari911 error_log("isMultiNamespace: " . ($isMultiNamespace ? 'true' : 'false')); 438e3a9f44cSAtari911 439e3a9f44cSAtari911 if ($isMultiNamespace) { 440e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 441e3a9f44cSAtari911 } else { 442e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 443e3a9f44cSAtari911 } 444e3a9f44cSAtari911 445e3a9f44cSAtari911 error_log("Returning " . count($events) . " date keys"); 446e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 447e3a9f44cSAtari911 error_log(" dateKey=$dateKey has " . count($dayEvents) . " events"); 448e3a9f44cSAtari911 } 449e3a9f44cSAtari911 450e3a9f44cSAtari911 echo json_encode([ 451e3a9f44cSAtari911 'success' => true, 452e3a9f44cSAtari911 'year' => $year, 453e3a9f44cSAtari911 'month' => $month, 454e3a9f44cSAtari911 'events' => $events 455e3a9f44cSAtari911 ]); 456e3a9f44cSAtari911 } 457e3a9f44cSAtari911 458e3a9f44cSAtari911 private function loadEventsSingleNamespace($namespace, $year, $month) { 45919378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 46019378907SAtari911 if ($namespace) { 46119378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 46219378907SAtari911 } 46319378907SAtari911 $dataDir .= 'calendar/'; 46419378907SAtari911 465e3a9f44cSAtari911 // Load ONLY current month 46687ac9bf3SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 46719378907SAtari911 $events = []; 46819378907SAtari911 if (file_exists($eventFile)) { 46987ac9bf3SAtari911 $contents = file_get_contents($eventFile); 47087ac9bf3SAtari911 $decoded = json_decode($contents, true); 47187ac9bf3SAtari911 if (json_last_error() === JSON_ERROR_NONE) { 47287ac9bf3SAtari911 $events = $decoded; 47387ac9bf3SAtari911 } 47487ac9bf3SAtari911 } 47587ac9bf3SAtari911 476e3a9f44cSAtari911 return $events; 47787ac9bf3SAtari911 } 478e3a9f44cSAtari911 479e3a9f44cSAtari911 private function loadEventsMultiNamespace($namespaces, $year, $month) { 480e3a9f44cSAtari911 // Check for wildcard pattern 481e3a9f44cSAtari911 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 482e3a9f44cSAtari911 $baseNamespace = $matches[1]; 483e3a9f44cSAtari911 return $this->loadEventsWildcard($baseNamespace, $year, $month); 484e3a9f44cSAtari911 } 485e3a9f44cSAtari911 486e3a9f44cSAtari911 // Check for root wildcard 487e3a9f44cSAtari911 if ($namespaces === '*') { 488e3a9f44cSAtari911 return $this->loadEventsWildcard('', $year, $month); 489e3a9f44cSAtari911 } 490e3a9f44cSAtari911 491e3a9f44cSAtari911 // Parse namespace list (semicolon separated) 492e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespaces)); 493e3a9f44cSAtari911 494e3a9f44cSAtari911 // Load events from all namespaces 495e3a9f44cSAtari911 $allEvents = []; 496e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 497e3a9f44cSAtari911 $ns = trim($ns); 498e3a9f44cSAtari911 if (empty($ns)) continue; 499e3a9f44cSAtari911 500e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($ns, $year, $month); 501e3a9f44cSAtari911 502e3a9f44cSAtari911 // Add namespace tag to each event 503e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 504e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 505e3a9f44cSAtari911 $allEvents[$dateKey] = []; 506e3a9f44cSAtari911 } 507e3a9f44cSAtari911 foreach ($dayEvents as $event) { 508e3a9f44cSAtari911 $event['_namespace'] = $ns; 509e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 510e3a9f44cSAtari911 } 51187ac9bf3SAtari911 } 51287ac9bf3SAtari911 } 51387ac9bf3SAtari911 514e3a9f44cSAtari911 return $allEvents; 515e3a9f44cSAtari911 } 51619378907SAtari911 517e3a9f44cSAtari911 private function loadEventsWildcard($baseNamespace, $year, $month) { 518e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 519e3a9f44cSAtari911 if ($baseNamespace) { 520e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 521e3a9f44cSAtari911 } 522e3a9f44cSAtari911 523e3a9f44cSAtari911 $allEvents = []; 524e3a9f44cSAtari911 525e3a9f44cSAtari911 // First, load events from the base namespace itself 526e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($baseNamespace, $year, $month); 527e3a9f44cSAtari911 528e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 529e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 530e3a9f44cSAtari911 $allEvents[$dateKey] = []; 531e3a9f44cSAtari911 } 532e3a9f44cSAtari911 foreach ($dayEvents as $event) { 533e3a9f44cSAtari911 $event['_namespace'] = $baseNamespace; 534e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 535e3a9f44cSAtari911 } 536e3a9f44cSAtari911 } 537e3a9f44cSAtari911 538e3a9f44cSAtari911 // Recursively find all subdirectories 539e3a9f44cSAtari911 $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents); 540e3a9f44cSAtari911 541e3a9f44cSAtari911 return $allEvents; 542e3a9f44cSAtari911 } 543e3a9f44cSAtari911 544e3a9f44cSAtari911 private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) { 545e3a9f44cSAtari911 if (!is_dir($dir)) return; 546e3a9f44cSAtari911 547e3a9f44cSAtari911 $items = scandir($dir); 548e3a9f44cSAtari911 foreach ($items as $item) { 549e3a9f44cSAtari911 if ($item === '.' || $item === '..') continue; 550e3a9f44cSAtari911 551e3a9f44cSAtari911 $path = $dir . $item; 552e3a9f44cSAtari911 if (is_dir($path) && $item !== 'calendar') { 553e3a9f44cSAtari911 // This is a namespace directory 554e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 555e3a9f44cSAtari911 556e3a9f44cSAtari911 // Load events from this namespace 557e3a9f44cSAtari911 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 558e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 559e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 560e3a9f44cSAtari911 $allEvents[$dateKey] = []; 561e3a9f44cSAtari911 } 562e3a9f44cSAtari911 foreach ($dayEvents as $event) { 563e3a9f44cSAtari911 $event['_namespace'] = $namespace; 564e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 565e3a9f44cSAtari911 } 566e3a9f44cSAtari911 } 567e3a9f44cSAtari911 568e3a9f44cSAtari911 // Recurse into subdirectories 569e3a9f44cSAtari911 $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents); 570e3a9f44cSAtari911 } 571e3a9f44cSAtari911 } 57219378907SAtari911 } 57319378907SAtari911 57419378907SAtari911 private function toggleTaskComplete() { 57519378907SAtari911 global $INPUT; 57619378907SAtari911 57719378907SAtari911 $namespace = $INPUT->str('namespace', ''); 57819378907SAtari911 $date = $INPUT->str('date'); 57919378907SAtari911 $eventId = $INPUT->str('eventId'); 58019378907SAtari911 $completed = $INPUT->bool('completed', false); 58119378907SAtari911 582e3a9f44cSAtari911 // Find where the event actually lives 583e3a9f44cSAtari911 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 584e3a9f44cSAtari911 585e3a9f44cSAtari911 if ($storedNamespace === null) { 586e3a9f44cSAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 587e3a9f44cSAtari911 return; 588e3a9f44cSAtari911 } 589e3a9f44cSAtari911 590e3a9f44cSAtari911 // Use the found namespace 591e3a9f44cSAtari911 $namespace = $storedNamespace; 592e3a9f44cSAtari911 59319378907SAtari911 list($year, $month, $day) = explode('-', $date); 59419378907SAtari911 59519378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 59619378907SAtari911 if ($namespace) { 59719378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 59819378907SAtari911 } 59919378907SAtari911 $dataDir .= 'calendar/'; 60019378907SAtari911 60119378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 60219378907SAtari911 60319378907SAtari911 if (file_exists($eventFile)) { 60419378907SAtari911 $events = json_decode(file_get_contents($eventFile), true); 60519378907SAtari911 60619378907SAtari911 if (isset($events[$date])) { 60719378907SAtari911 foreach ($events[$date] as $key => $event) { 60819378907SAtari911 if ($event['id'] === $eventId) { 60919378907SAtari911 $events[$date][$key]['completed'] = $completed; 61019378907SAtari911 break; 61119378907SAtari911 } 61219378907SAtari911 } 61319378907SAtari911 61419378907SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 61519378907SAtari911 echo json_encode(['success' => true, 'events' => $events]); 61619378907SAtari911 return; 61719378907SAtari911 } 61819378907SAtari911 } 61919378907SAtari911 62019378907SAtari911 echo json_encode(['success' => false, 'error' => 'Event not found']); 62119378907SAtari911 } 62219378907SAtari911 62387ac9bf3SAtari911 private function createRecurringEvents($namespace, $startDate, $endDate, $title, $time, 62487ac9bf3SAtari911 $description, $color, $isTask, $recurrenceType, 62587ac9bf3SAtari911 $recurrenceEnd, $baseId) { 62687ac9bf3SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 62787ac9bf3SAtari911 if ($namespace) { 62887ac9bf3SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 62987ac9bf3SAtari911 } 63087ac9bf3SAtari911 $dataDir .= 'calendar/'; 63187ac9bf3SAtari911 63287ac9bf3SAtari911 if (!is_dir($dataDir)) { 63387ac9bf3SAtari911 mkdir($dataDir, 0755, true); 63487ac9bf3SAtari911 } 63587ac9bf3SAtari911 63687ac9bf3SAtari911 // Calculate recurrence interval 63787ac9bf3SAtari911 $interval = ''; 63887ac9bf3SAtari911 switch ($recurrenceType) { 63987ac9bf3SAtari911 case 'daily': $interval = '+1 day'; break; 64087ac9bf3SAtari911 case 'weekly': $interval = '+1 week'; break; 64187ac9bf3SAtari911 case 'monthly': $interval = '+1 month'; break; 64287ac9bf3SAtari911 case 'yearly': $interval = '+1 year'; break; 64387ac9bf3SAtari911 default: $interval = '+1 week'; 64487ac9bf3SAtari911 } 64587ac9bf3SAtari911 64687ac9bf3SAtari911 // Set maximum end date if not specified (1 year from start) 64787ac9bf3SAtari911 $maxEnd = $recurrenceEnd ?: date('Y-m-d', strtotime($startDate . ' +1 year')); 64887ac9bf3SAtari911 64987ac9bf3SAtari911 // Calculate event duration for multi-day events 65087ac9bf3SAtari911 $eventDuration = 0; 65187ac9bf3SAtari911 if ($endDate && $endDate !== $startDate) { 65287ac9bf3SAtari911 $start = new DateTime($startDate); 65387ac9bf3SAtari911 $end = new DateTime($endDate); 65487ac9bf3SAtari911 $eventDuration = $start->diff($end)->days; 65587ac9bf3SAtari911 } 65687ac9bf3SAtari911 65787ac9bf3SAtari911 // Generate recurring events 65887ac9bf3SAtari911 $currentDate = new DateTime($startDate); 65987ac9bf3SAtari911 $endLimit = new DateTime($maxEnd); 66087ac9bf3SAtari911 $counter = 0; 66187ac9bf3SAtari911 $maxOccurrences = 100; // Prevent infinite loops 66287ac9bf3SAtari911 66387ac9bf3SAtari911 while ($currentDate <= $endLimit && $counter < $maxOccurrences) { 66487ac9bf3SAtari911 $dateKey = $currentDate->format('Y-m-d'); 66587ac9bf3SAtari911 list($year, $month, $day) = explode('-', $dateKey); 66687ac9bf3SAtari911 66787ac9bf3SAtari911 // Calculate end date for this occurrence if multi-day 66887ac9bf3SAtari911 $occurrenceEndDate = ''; 66987ac9bf3SAtari911 if ($eventDuration > 0) { 67087ac9bf3SAtari911 $occurrenceEnd = clone $currentDate; 67187ac9bf3SAtari911 $occurrenceEnd->modify('+' . $eventDuration . ' days'); 67287ac9bf3SAtari911 $occurrenceEndDate = $occurrenceEnd->format('Y-m-d'); 67387ac9bf3SAtari911 } 67487ac9bf3SAtari911 67587ac9bf3SAtari911 // Load month file 67687ac9bf3SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 67787ac9bf3SAtari911 $events = []; 67887ac9bf3SAtari911 if (file_exists($eventFile)) { 67987ac9bf3SAtari911 $events = json_decode(file_get_contents($eventFile), true); 68087ac9bf3SAtari911 } 68187ac9bf3SAtari911 68287ac9bf3SAtari911 if (!isset($events[$dateKey])) { 68387ac9bf3SAtari911 $events[$dateKey] = []; 68487ac9bf3SAtari911 } 68587ac9bf3SAtari911 68687ac9bf3SAtari911 // Create event for this occurrence 68787ac9bf3SAtari911 $eventData = [ 68887ac9bf3SAtari911 'id' => $baseId . '-' . $counter, 68987ac9bf3SAtari911 'title' => $title, 69087ac9bf3SAtari911 'time' => $time, 691*1d05cddcSAtari911 'endTime' => $endTime, 69287ac9bf3SAtari911 'description' => $description, 69387ac9bf3SAtari911 'color' => $color, 69487ac9bf3SAtari911 'isTask' => $isTask, 69587ac9bf3SAtari911 'completed' => false, 69687ac9bf3SAtari911 'endDate' => $occurrenceEndDate, 69787ac9bf3SAtari911 'recurring' => true, 69887ac9bf3SAtari911 'recurringId' => $baseId, 699*1d05cddcSAtari911 'namespace' => $namespace, // Add namespace! 70087ac9bf3SAtari911 'created' => date('Y-m-d H:i:s') 70187ac9bf3SAtari911 ]; 70287ac9bf3SAtari911 70387ac9bf3SAtari911 $events[$dateKey][] = $eventData; 70487ac9bf3SAtari911 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 70587ac9bf3SAtari911 70687ac9bf3SAtari911 // Move to next occurrence 70787ac9bf3SAtari911 $currentDate->modify($interval); 70887ac9bf3SAtari911 $counter++; 70987ac9bf3SAtari911 } 71087ac9bf3SAtari911 } 71187ac9bf3SAtari911 71219378907SAtari911 public function addAssets(Doku_Event $event, $param) { 71319378907SAtari911 $event->data['link'][] = array( 71419378907SAtari911 'type' => 'text/css', 71519378907SAtari911 'rel' => 'stylesheet', 71619378907SAtari911 'href' => DOKU_BASE . 'lib/plugins/calendar/style.css' 71719378907SAtari911 ); 71819378907SAtari911 71919378907SAtari911 $event->data['script'][] = array( 72019378907SAtari911 'type' => 'text/javascript', 72119378907SAtari911 'src' => DOKU_BASE . 'lib/plugins/calendar/script.js' 72219378907SAtari911 ); 72319378907SAtari911 } 724e3a9f44cSAtari911 // Helper function to find an event's stored namespace 725e3a9f44cSAtari911 private function findEventNamespace($eventId, $date, $searchNamespace) { 726e3a9f44cSAtari911 list($year, $month, $day) = explode('-', $date); 727e3a9f44cSAtari911 728e3a9f44cSAtari911 // List of namespaces to check 729e3a9f44cSAtari911 $namespacesToCheck = ['']; 730e3a9f44cSAtari911 731e3a9f44cSAtari911 // If searchNamespace is a wildcard or multi, we need to search multiple locations 732e3a9f44cSAtari911 if (!empty($searchNamespace)) { 733e3a9f44cSAtari911 if (strpos($searchNamespace, ';') !== false) { 734e3a9f44cSAtari911 // Multi-namespace - check each one 735e3a9f44cSAtari911 $namespacesToCheck = array_map('trim', explode(';', $searchNamespace)); 736e3a9f44cSAtari911 $namespacesToCheck[] = ''; // Also check default 737e3a9f44cSAtari911 } elseif (strpos($searchNamespace, '*') !== false) { 738e3a9f44cSAtari911 // Wildcard - need to scan directories 739e3a9f44cSAtari911 $baseNs = trim(str_replace('*', '', $searchNamespace), ':'); 740e3a9f44cSAtari911 $namespacesToCheck = $this->findAllNamespaces($baseNs); 741e3a9f44cSAtari911 $namespacesToCheck[] = ''; // Also check default 742e3a9f44cSAtari911 } else { 743e3a9f44cSAtari911 // Single namespace 744e3a9f44cSAtari911 $namespacesToCheck = [$searchNamespace, '']; // Check specified and default 745e3a9f44cSAtari911 } 746e3a9f44cSAtari911 } 747e3a9f44cSAtari911 748e3a9f44cSAtari911 // Search for the event in all possible namespaces 749e3a9f44cSAtari911 foreach ($namespacesToCheck as $ns) { 750e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 751e3a9f44cSAtari911 if ($ns) { 752e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $ns) . '/'; 753e3a9f44cSAtari911 } 754e3a9f44cSAtari911 $dataDir .= 'calendar/'; 755e3a9f44cSAtari911 756e3a9f44cSAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 757e3a9f44cSAtari911 758e3a9f44cSAtari911 if (file_exists($eventFile)) { 759e3a9f44cSAtari911 $events = json_decode(file_get_contents($eventFile), true); 760e3a9f44cSAtari911 if (isset($events[$date])) { 761e3a9f44cSAtari911 foreach ($events[$date] as $evt) { 762e3a9f44cSAtari911 if ($evt['id'] === $eventId) { 763e3a9f44cSAtari911 // Found the event! Return its stored namespace 764e3a9f44cSAtari911 return isset($evt['namespace']) ? $evt['namespace'] : $ns; 765e3a9f44cSAtari911 } 766e3a9f44cSAtari911 } 767e3a9f44cSAtari911 } 768e3a9f44cSAtari911 } 769e3a9f44cSAtari911 } 770e3a9f44cSAtari911 771e3a9f44cSAtari911 return null; // Event not found 772e3a9f44cSAtari911 } 773e3a9f44cSAtari911 774e3a9f44cSAtari911 // Helper to find all namespaces under a base namespace 775e3a9f44cSAtari911 private function findAllNamespaces($baseNamespace) { 776e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 777e3a9f44cSAtari911 if ($baseNamespace) { 778e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 779e3a9f44cSAtari911 } 780e3a9f44cSAtari911 781e3a9f44cSAtari911 $namespaces = []; 782e3a9f44cSAtari911 if ($baseNamespace) { 783e3a9f44cSAtari911 $namespaces[] = $baseNamespace; 784e3a9f44cSAtari911 } 785e3a9f44cSAtari911 786e3a9f44cSAtari911 $this->scanForNamespaces($dataDir, $baseNamespace, $namespaces); 787e3a9f44cSAtari911 788e3a9f44cSAtari911 return $namespaces; 789e3a9f44cSAtari911 } 790e3a9f44cSAtari911 791e3a9f44cSAtari911 // Recursive scan for namespaces 792e3a9f44cSAtari911 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 793e3a9f44cSAtari911 if (!is_dir($dir)) return; 794e3a9f44cSAtari911 795e3a9f44cSAtari911 $items = scandir($dir); 796e3a9f44cSAtari911 foreach ($items as $item) { 797e3a9f44cSAtari911 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 798e3a9f44cSAtari911 799e3a9f44cSAtari911 $path = $dir . $item; 800e3a9f44cSAtari911 if (is_dir($path)) { 801e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 802e3a9f44cSAtari911 $namespaces[] = $namespace; 803e3a9f44cSAtari911 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 804e3a9f44cSAtari911 } 805e3a9f44cSAtari911 } 806e3a9f44cSAtari911 } 80719378907SAtari911} 808