1<?php 2/** 3 * DokuWiki Plugin calendar (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author DokuWiki Community 7 */ 8 9if (!defined('DOKU_INC')) die(); 10 11class action_plugin_calendar extends DokuWiki_Action_Plugin { 12 13 public function register(Doku_Event_Handler $controller) { 14 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax'); 15 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addAssets'); 16 } 17 18 public function handleAjax(Doku_Event $event, $param) { 19 if ($event->data !== 'plugin_calendar') return; 20 $event->preventDefault(); 21 $event->stopPropagation(); 22 23 $action = $_REQUEST['action'] ?? ''; 24 25 switch ($action) { 26 case 'save_event': 27 $this->saveEvent(); 28 break; 29 case 'delete_event': 30 $this->deleteEvent(); 31 break; 32 case 'get_event': 33 $this->getEvent(); 34 break; 35 case 'load_month': 36 $this->loadMonth(); 37 break; 38 case 'toggle_task': 39 $this->toggleTaskComplete(); 40 break; 41 default: 42 echo json_encode(['success' => false, 'error' => 'Unknown action']); 43 } 44 } 45 46 private function saveEvent() { 47 global $INPUT; 48 49 $namespace = $INPUT->str('namespace', ''); 50 $date = $INPUT->str('date'); 51 $eventId = $INPUT->str('eventId', ''); 52 $title = $INPUT->str('title'); 53 $time = $INPUT->str('time', ''); 54 $endTime = $INPUT->str('endTime', ''); 55 $description = $INPUT->str('description', ''); 56 $color = $INPUT->str('color', '#3498db'); 57 $oldDate = $INPUT->str('oldDate', ''); // Track original date for moves 58 $isTask = $INPUT->bool('isTask', false); 59 $completed = $INPUT->bool('completed', false); 60 $endDate = $INPUT->str('endDate', ''); 61 $isRecurring = $INPUT->bool('isRecurring', false); 62 $recurrenceType = $INPUT->str('recurrenceType', 'weekly'); 63 $recurrenceEnd = $INPUT->str('recurrenceEnd', ''); 64 65 if (!$date || !$title) { 66 echo json_encode(['success' => false, 'error' => 'Missing required fields']); 67 return; 68 } 69 70 // If editing, find the event's stored namespace (for finding/deleting old event) 71 $storedNamespace = ''; 72 $oldNamespace = ''; 73 if ($eventId) { 74 // Use oldDate if available (date was changed), otherwise use current date 75 $searchDate = ($oldDate && $oldDate !== $date) ? $oldDate : $date; 76 $storedNamespace = $this->findEventNamespace($eventId, $searchDate, $namespace); 77 78 // Store the old namespace for deletion purposes 79 if ($storedNamespace !== null) { 80 $oldNamespace = $storedNamespace; 81 error_log("Calendar saveEvent: Found existing event in namespace '$oldNamespace'"); 82 } 83 } 84 85 // Use the namespace provided by the user (allow namespace changes!) 86 // But normalize wildcards and multi-namespace to empty for NEW events 87 if (!$eventId) { 88 error_log("Calendar saveEvent: NEW event, received namespace='$namespace'"); 89 // Normalize namespace: treat wildcards and multi-namespace as empty (default) for NEW events 90 if (!empty($namespace) && (strpos($namespace, '*') !== false || strpos($namespace, ';') !== false)) { 91 error_log("Calendar saveEvent: Namespace contains wildcard/multi, clearing to empty"); 92 $namespace = ''; 93 } else { 94 error_log("Calendar saveEvent: Namespace is clean, keeping as '$namespace'"); 95 } 96 } else { 97 error_log("Calendar saveEvent: EDITING event $eventId, user selected namespace='$namespace'"); 98 } 99 100 // Generate event ID if new 101 $generatedId = $eventId ?: uniqid(); 102 103 // If recurring, generate multiple events 104 if ($isRecurring) { 105 $this->createRecurringEvents($namespace, $date, $endDate, $title, $time, $description, 106 $color, $isTask, $recurrenceType, $recurrenceEnd, $generatedId); 107 echo json_encode(['success' => true]); 108 return; 109 } 110 111 list($year, $month, $day) = explode('-', $date); 112 113 // NEW namespace directory (where we'll save) 114 $dataDir = DOKU_INC . 'data/meta/'; 115 if ($namespace) { 116 $dataDir .= str_replace(':', '/', $namespace) . '/'; 117 } 118 $dataDir .= 'calendar/'; 119 120 if (!is_dir($dataDir)) { 121 mkdir($dataDir, 0755, true); 122 } 123 124 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 125 126 $events = []; 127 if (file_exists($eventFile)) { 128 $events = json_decode(file_get_contents($eventFile), true); 129 } 130 131 // If editing and (date changed OR namespace changed), remove from old location first 132 $namespaceChanged = ($eventId && $oldNamespace !== '' && $oldNamespace !== $namespace); 133 $dateChanged = ($eventId && $oldDate && $oldDate !== $date); 134 135 if ($namespaceChanged || $dateChanged) { 136 // Construct OLD data directory using OLD namespace 137 $oldDataDir = DOKU_INC . 'data/meta/'; 138 if ($oldNamespace) { 139 $oldDataDir .= str_replace(':', '/', $oldNamespace) . '/'; 140 } 141 $oldDataDir .= 'calendar/'; 142 143 $deleteDate = $dateChanged ? $oldDate : $date; 144 list($oldYear, $oldMonth, $oldDay) = explode('-', $deleteDate); 145 $oldEventFile = $oldDataDir . sprintf('%04d-%02d.json', $oldYear, $oldMonth); 146 147 if (file_exists($oldEventFile)) { 148 $oldEvents = json_decode(file_get_contents($oldEventFile), true); 149 if (isset($oldEvents[$deleteDate])) { 150 $oldEvents[$deleteDate] = array_values(array_filter($oldEvents[$deleteDate], function($evt) use ($eventId) { 151 return $evt['id'] !== $eventId; 152 })); 153 154 if (empty($oldEvents[$deleteDate])) { 155 unset($oldEvents[$deleteDate]); 156 } 157 158 file_put_contents($oldEventFile, json_encode($oldEvents, JSON_PRETTY_PRINT)); 159 error_log("Calendar saveEvent: Deleted event from old location - namespace:'$oldNamespace', date:'$deleteDate'"); 160 } 161 } 162 } 163 164 if (!isset($events[$date])) { 165 $events[$date] = []; 166 } elseif (!is_array($events[$date])) { 167 // Fix corrupted data - ensure it's an array 168 error_log("Calendar saveEvent: Fixing corrupted data at $date - was not an array"); 169 $events[$date] = []; 170 } 171 172 // Store the namespace with the event 173 $eventData = [ 174 'id' => $generatedId, 175 'title' => $title, 176 'time' => $time, 177 'endTime' => $endTime, 178 'description' => $description, 179 'color' => $color, 180 'isTask' => $isTask, 181 'completed' => $completed, 182 'endDate' => $endDate, 183 'namespace' => $namespace, // Store namespace with event 184 'created' => date('Y-m-d H:i:s') 185 ]; 186 187 // Debug logging 188 error_log("Calendar saveEvent: Saving event '$title' with namespace='$namespace' to file $eventFile"); 189 190 // If editing, replace existing event 191 if ($eventId) { 192 $found = false; 193 foreach ($events[$date] as $key => $evt) { 194 if ($evt['id'] === $eventId) { 195 $events[$date][$key] = $eventData; 196 $found = true; 197 break; 198 } 199 } 200 if (!$found) { 201 $events[$date][] = $eventData; 202 } 203 } else { 204 $events[$date][] = $eventData; 205 } 206 207 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 208 209 // If event spans multiple months, add it to the first day of each subsequent month 210 if ($endDate && $endDate !== $date) { 211 $startDateObj = new DateTime($date); 212 $endDateObj = new DateTime($endDate); 213 214 // Get the month/year of the start date 215 $startMonth = $startDateObj->format('Y-m'); 216 217 // Iterate through each month the event spans 218 $currentDate = clone $startDateObj; 219 $currentDate->modify('first day of next month'); // Jump to first of next month 220 221 while ($currentDate <= $endDateObj) { 222 $currentMonth = $currentDate->format('Y-m'); 223 $firstDayOfMonth = $currentDate->format('Y-m-01'); 224 225 list($currentYear, $currentMonthNum, $currentDay) = explode('-', $firstDayOfMonth); 226 227 // Get the file for this month 228 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonthNum); 229 230 $currentEvents = []; 231 if (file_exists($currentEventFile)) { 232 $contents = file_get_contents($currentEventFile); 233 $decoded = json_decode($contents, true); 234 if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { 235 $currentEvents = $decoded; 236 } else { 237 error_log("Calendar saveEvent: JSON decode error in $currentEventFile: " . json_last_error_msg()); 238 } 239 } 240 241 // Add entry for the first day of this month 242 if (!isset($currentEvents[$firstDayOfMonth])) { 243 $currentEvents[$firstDayOfMonth] = []; 244 } elseif (!is_array($currentEvents[$firstDayOfMonth])) { 245 // Fix corrupted data - ensure it's an array 246 error_log("Calendar saveEvent: Fixing corrupted data at $firstDayOfMonth - was not an array"); 247 $currentEvents[$firstDayOfMonth] = []; 248 } 249 250 // Create a copy with the original start date preserved 251 $eventDataForMonth = $eventData; 252 $eventDataForMonth['originalStartDate'] = $date; // Preserve the actual start date 253 254 // Check if event already exists (when editing) 255 $found = false; 256 if ($eventId) { 257 foreach ($currentEvents[$firstDayOfMonth] as $key => $evt) { 258 if ($evt['id'] === $eventId) { 259 $currentEvents[$firstDayOfMonth][$key] = $eventDataForMonth; 260 $found = true; 261 break; 262 } 263 } 264 } 265 266 if (!$found) { 267 $currentEvents[$firstDayOfMonth][] = $eventDataForMonth; 268 } 269 270 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 271 272 // Move to next month 273 $currentDate->modify('first day of next month'); 274 } 275 } 276 277 echo json_encode(['success' => true, 'events' => $events, 'eventId' => $eventData['id']]); 278 } 279 280 private function deleteEvent() { 281 global $INPUT; 282 283 $namespace = $INPUT->str('namespace', ''); 284 $date = $INPUT->str('date'); 285 $eventId = $INPUT->str('eventId'); 286 287 // Find where the event actually lives 288 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 289 290 if ($storedNamespace === null) { 291 echo json_encode(['success' => false, 'error' => 'Event not found']); 292 return; 293 } 294 295 // Use the found namespace 296 $namespace = $storedNamespace; 297 298 list($year, $month, $day) = explode('-', $date); 299 300 $dataDir = DOKU_INC . 'data/meta/'; 301 if ($namespace) { 302 $dataDir .= str_replace(':', '/', $namespace) . '/'; 303 } 304 $dataDir .= 'calendar/'; 305 306 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 307 308 // First, get the event to check if it spans multiple months 309 $eventToDelete = null; 310 if (file_exists($eventFile)) { 311 $events = json_decode(file_get_contents($eventFile), true); 312 313 if (isset($events[$date])) { 314 foreach ($events[$date] as $event) { 315 if ($event['id'] === $eventId) { 316 $eventToDelete = $event; 317 break; 318 } 319 } 320 321 $events[$date] = array_values(array_filter($events[$date], function($event) use ($eventId) { 322 return $event['id'] !== $eventId; 323 })); 324 325 if (empty($events[$date])) { 326 unset($events[$date]); 327 } 328 329 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 330 } 331 } 332 333 // If event spans multiple months, delete it from the first day of each subsequent month 334 if ($eventToDelete && isset($eventToDelete['endDate']) && $eventToDelete['endDate'] && $eventToDelete['endDate'] !== $date) { 335 $startDateObj = new DateTime($date); 336 $endDateObj = new DateTime($eventToDelete['endDate']); 337 338 // Iterate through each month the event spans 339 $currentDate = clone $startDateObj; 340 $currentDate->modify('first day of next month'); // Jump to first of next month 341 342 while ($currentDate <= $endDateObj) { 343 $firstDayOfMonth = $currentDate->format('Y-m-01'); 344 list($currentYear, $currentMonth, $currentDay) = explode('-', $firstDayOfMonth); 345 346 // Get the file for this month 347 $currentEventFile = $dataDir . sprintf('%04d-%02d.json', $currentYear, $currentMonth); 348 349 if (file_exists($currentEventFile)) { 350 $currentEvents = json_decode(file_get_contents($currentEventFile), true); 351 352 if (isset($currentEvents[$firstDayOfMonth])) { 353 $currentEvents[$firstDayOfMonth] = array_values(array_filter($currentEvents[$firstDayOfMonth], function($event) use ($eventId) { 354 return $event['id'] !== $eventId; 355 })); 356 357 if (empty($currentEvents[$firstDayOfMonth])) { 358 unset($currentEvents[$firstDayOfMonth]); 359 } 360 361 file_put_contents($currentEventFile, json_encode($currentEvents, JSON_PRETTY_PRINT)); 362 } 363 } 364 365 // Move to next month 366 $currentDate->modify('first day of next month'); 367 } 368 } 369 370 echo json_encode(['success' => true]); 371 } 372 373 private function getEvent() { 374 global $INPUT; 375 376 $namespace = $INPUT->str('namespace', ''); 377 $date = $INPUT->str('date'); 378 $eventId = $INPUT->str('eventId'); 379 380 // Find where the event actually lives 381 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 382 383 if ($storedNamespace === null) { 384 echo json_encode(['success' => false, 'error' => 'Event not found']); 385 return; 386 } 387 388 // Use the found namespace 389 $namespace = $storedNamespace; 390 391 list($year, $month, $day) = explode('-', $date); 392 393 $dataDir = DOKU_INC . 'data/meta/'; 394 if ($namespace) { 395 $dataDir .= str_replace(':', '/', $namespace) . '/'; 396 } 397 $dataDir .= 'calendar/'; 398 399 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 400 401 if (file_exists($eventFile)) { 402 $events = json_decode(file_get_contents($eventFile), true); 403 404 if (isset($events[$date])) { 405 foreach ($events[$date] as $event) { 406 if ($event['id'] === $eventId) { 407 // Include the namespace so JavaScript knows where this event actually lives 408 $event['namespace'] = $namespace; 409 echo json_encode(['success' => true, 'event' => $event]); 410 return; 411 } 412 } 413 } 414 } 415 416 echo json_encode(['success' => false, 'error' => 'Event not found']); 417 } 418 419 private function loadMonth() { 420 global $INPUT; 421 422 // Prevent caching of AJAX responses 423 header('Cache-Control: no-cache, no-store, must-revalidate'); 424 header('Pragma: no-cache'); 425 header('Expires: 0'); 426 427 $namespace = $INPUT->str('namespace', ''); 428 $year = $INPUT->int('year'); 429 $month = $INPUT->int('month'); 430 431 error_log("=== Calendar loadMonth DEBUG ==="); 432 error_log("Requested: year=$year, month=$month, namespace='$namespace'"); 433 434 // Check if multi-namespace or wildcard 435 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 436 437 error_log("isMultiNamespace: " . ($isMultiNamespace ? 'true' : 'false')); 438 439 if ($isMultiNamespace) { 440 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 441 } else { 442 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 443 } 444 445 error_log("Returning " . count($events) . " date keys"); 446 foreach ($events as $dateKey => $dayEvents) { 447 error_log(" dateKey=$dateKey has " . count($dayEvents) . " events"); 448 } 449 450 echo json_encode([ 451 'success' => true, 452 'year' => $year, 453 'month' => $month, 454 'events' => $events 455 ]); 456 } 457 458 private function loadEventsSingleNamespace($namespace, $year, $month) { 459 $dataDir = DOKU_INC . 'data/meta/'; 460 if ($namespace) { 461 $dataDir .= str_replace(':', '/', $namespace) . '/'; 462 } 463 $dataDir .= 'calendar/'; 464 465 // Load ONLY current month 466 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 467 $events = []; 468 if (file_exists($eventFile)) { 469 $contents = file_get_contents($eventFile); 470 $decoded = json_decode($contents, true); 471 if (json_last_error() === JSON_ERROR_NONE) { 472 $events = $decoded; 473 } 474 } 475 476 return $events; 477 } 478 479 private function loadEventsMultiNamespace($namespaces, $year, $month) { 480 // Check for wildcard pattern 481 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 482 $baseNamespace = $matches[1]; 483 return $this->loadEventsWildcard($baseNamespace, $year, $month); 484 } 485 486 // Check for root wildcard 487 if ($namespaces === '*') { 488 return $this->loadEventsWildcard('', $year, $month); 489 } 490 491 // Parse namespace list (semicolon separated) 492 $namespaceList = array_map('trim', explode(';', $namespaces)); 493 494 // Load events from all namespaces 495 $allEvents = []; 496 foreach ($namespaceList as $ns) { 497 $ns = trim($ns); 498 if (empty($ns)) continue; 499 500 $events = $this->loadEventsSingleNamespace($ns, $year, $month); 501 502 // Add namespace tag to each event 503 foreach ($events as $dateKey => $dayEvents) { 504 if (!isset($allEvents[$dateKey])) { 505 $allEvents[$dateKey] = []; 506 } 507 foreach ($dayEvents as $event) { 508 $event['_namespace'] = $ns; 509 $allEvents[$dateKey][] = $event; 510 } 511 } 512 } 513 514 return $allEvents; 515 } 516 517 private function loadEventsWildcard($baseNamespace, $year, $month) { 518 $dataDir = DOKU_INC . 'data/meta/'; 519 if ($baseNamespace) { 520 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 521 } 522 523 $allEvents = []; 524 525 // First, load events from the base namespace itself 526 $events = $this->loadEventsSingleNamespace($baseNamespace, $year, $month); 527 528 foreach ($events as $dateKey => $dayEvents) { 529 if (!isset($allEvents[$dateKey])) { 530 $allEvents[$dateKey] = []; 531 } 532 foreach ($dayEvents as $event) { 533 $event['_namespace'] = $baseNamespace; 534 $allEvents[$dateKey][] = $event; 535 } 536 } 537 538 // Recursively find all subdirectories 539 $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents); 540 541 return $allEvents; 542 } 543 544 private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) { 545 if (!is_dir($dir)) return; 546 547 $items = scandir($dir); 548 foreach ($items as $item) { 549 if ($item === '.' || $item === '..') continue; 550 551 $path = $dir . $item; 552 if (is_dir($path) && $item !== 'calendar') { 553 // This is a namespace directory 554 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 555 556 // Load events from this namespace 557 $events = $this->loadEventsSingleNamespace($namespace, $year, $month); 558 foreach ($events as $dateKey => $dayEvents) { 559 if (!isset($allEvents[$dateKey])) { 560 $allEvents[$dateKey] = []; 561 } 562 foreach ($dayEvents as $event) { 563 $event['_namespace'] = $namespace; 564 $allEvents[$dateKey][] = $event; 565 } 566 } 567 568 // Recurse into subdirectories 569 $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents); 570 } 571 } 572 } 573 574 private function toggleTaskComplete() { 575 global $INPUT; 576 577 $namespace = $INPUT->str('namespace', ''); 578 $date = $INPUT->str('date'); 579 $eventId = $INPUT->str('eventId'); 580 $completed = $INPUT->bool('completed', false); 581 582 // Find where the event actually lives 583 $storedNamespace = $this->findEventNamespace($eventId, $date, $namespace); 584 585 if ($storedNamespace === null) { 586 echo json_encode(['success' => false, 'error' => 'Event not found']); 587 return; 588 } 589 590 // Use the found namespace 591 $namespace = $storedNamespace; 592 593 list($year, $month, $day) = explode('-', $date); 594 595 $dataDir = DOKU_INC . 'data/meta/'; 596 if ($namespace) { 597 $dataDir .= str_replace(':', '/', $namespace) . '/'; 598 } 599 $dataDir .= 'calendar/'; 600 601 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 602 603 if (file_exists($eventFile)) { 604 $events = json_decode(file_get_contents($eventFile), true); 605 606 if (isset($events[$date])) { 607 foreach ($events[$date] as $key => $event) { 608 if ($event['id'] === $eventId) { 609 $events[$date][$key]['completed'] = $completed; 610 break; 611 } 612 } 613 614 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 615 echo json_encode(['success' => true, 'events' => $events]); 616 return; 617 } 618 } 619 620 echo json_encode(['success' => false, 'error' => 'Event not found']); 621 } 622 623 private function createRecurringEvents($namespace, $startDate, $endDate, $title, $time, 624 $description, $color, $isTask, $recurrenceType, 625 $recurrenceEnd, $baseId) { 626 $dataDir = DOKU_INC . 'data/meta/'; 627 if ($namespace) { 628 $dataDir .= str_replace(':', '/', $namespace) . '/'; 629 } 630 $dataDir .= 'calendar/'; 631 632 if (!is_dir($dataDir)) { 633 mkdir($dataDir, 0755, true); 634 } 635 636 // Calculate recurrence interval 637 $interval = ''; 638 switch ($recurrenceType) { 639 case 'daily': $interval = '+1 day'; break; 640 case 'weekly': $interval = '+1 week'; break; 641 case 'monthly': $interval = '+1 month'; break; 642 case 'yearly': $interval = '+1 year'; break; 643 default: $interval = '+1 week'; 644 } 645 646 // Set maximum end date if not specified (1 year from start) 647 $maxEnd = $recurrenceEnd ?: date('Y-m-d', strtotime($startDate . ' +1 year')); 648 649 // Calculate event duration for multi-day events 650 $eventDuration = 0; 651 if ($endDate && $endDate !== $startDate) { 652 $start = new DateTime($startDate); 653 $end = new DateTime($endDate); 654 $eventDuration = $start->diff($end)->days; 655 } 656 657 // Generate recurring events 658 $currentDate = new DateTime($startDate); 659 $endLimit = new DateTime($maxEnd); 660 $counter = 0; 661 $maxOccurrences = 100; // Prevent infinite loops 662 663 while ($currentDate <= $endLimit && $counter < $maxOccurrences) { 664 $dateKey = $currentDate->format('Y-m-d'); 665 list($year, $month, $day) = explode('-', $dateKey); 666 667 // Calculate end date for this occurrence if multi-day 668 $occurrenceEndDate = ''; 669 if ($eventDuration > 0) { 670 $occurrenceEnd = clone $currentDate; 671 $occurrenceEnd->modify('+' . $eventDuration . ' days'); 672 $occurrenceEndDate = $occurrenceEnd->format('Y-m-d'); 673 } 674 675 // Load month file 676 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 677 $events = []; 678 if (file_exists($eventFile)) { 679 $events = json_decode(file_get_contents($eventFile), true); 680 } 681 682 if (!isset($events[$dateKey])) { 683 $events[$dateKey] = []; 684 } 685 686 // Create event for this occurrence 687 $eventData = [ 688 'id' => $baseId . '-' . $counter, 689 'title' => $title, 690 'time' => $time, 691 'endTime' => $endTime, 692 'description' => $description, 693 'color' => $color, 694 'isTask' => $isTask, 695 'completed' => false, 696 'endDate' => $occurrenceEndDate, 697 'recurring' => true, 698 'recurringId' => $baseId, 699 'namespace' => $namespace, // Add namespace! 700 'created' => date('Y-m-d H:i:s') 701 ]; 702 703 $events[$dateKey][] = $eventData; 704 file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT)); 705 706 // Move to next occurrence 707 $currentDate->modify($interval); 708 $counter++; 709 } 710 } 711 712 public function addAssets(Doku_Event $event, $param) { 713 $event->data['link'][] = array( 714 'type' => 'text/css', 715 'rel' => 'stylesheet', 716 'href' => DOKU_BASE . 'lib/plugins/calendar/style.css' 717 ); 718 719 $event->data['script'][] = array( 720 'type' => 'text/javascript', 721 'src' => DOKU_BASE . 'lib/plugins/calendar/script.js' 722 ); 723 } 724 // Helper function to find an event's stored namespace 725 private function findEventNamespace($eventId, $date, $searchNamespace) { 726 list($year, $month, $day) = explode('-', $date); 727 728 // List of namespaces to check 729 $namespacesToCheck = ['']; 730 731 // If searchNamespace is a wildcard or multi, we need to search multiple locations 732 if (!empty($searchNamespace)) { 733 if (strpos($searchNamespace, ';') !== false) { 734 // Multi-namespace - check each one 735 $namespacesToCheck = array_map('trim', explode(';', $searchNamespace)); 736 $namespacesToCheck[] = ''; // Also check default 737 } elseif (strpos($searchNamespace, '*') !== false) { 738 // Wildcard - need to scan directories 739 $baseNs = trim(str_replace('*', '', $searchNamespace), ':'); 740 $namespacesToCheck = $this->findAllNamespaces($baseNs); 741 $namespacesToCheck[] = ''; // Also check default 742 } else { 743 // Single namespace 744 $namespacesToCheck = [$searchNamespace, '']; // Check specified and default 745 } 746 } 747 748 // Search for the event in all possible namespaces 749 foreach ($namespacesToCheck as $ns) { 750 $dataDir = DOKU_INC . 'data/meta/'; 751 if ($ns) { 752 $dataDir .= str_replace(':', '/', $ns) . '/'; 753 } 754 $dataDir .= 'calendar/'; 755 756 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 757 758 if (file_exists($eventFile)) { 759 $events = json_decode(file_get_contents($eventFile), true); 760 if (isset($events[$date])) { 761 foreach ($events[$date] as $evt) { 762 if ($evt['id'] === $eventId) { 763 // Found the event! Return its stored namespace 764 return isset($evt['namespace']) ? $evt['namespace'] : $ns; 765 } 766 } 767 } 768 } 769 } 770 771 return null; // Event not found 772 } 773 774 // Helper to find all namespaces under a base namespace 775 private function findAllNamespaces($baseNamespace) { 776 $dataDir = DOKU_INC . 'data/meta/'; 777 if ($baseNamespace) { 778 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 779 } 780 781 $namespaces = []; 782 if ($baseNamespace) { 783 $namespaces[] = $baseNamespace; 784 } 785 786 $this->scanForNamespaces($dataDir, $baseNamespace, $namespaces); 787 788 return $namespaces; 789 } 790 791 // Recursive scan for namespaces 792 private function scanForNamespaces($dir, $baseNamespace, &$namespaces) { 793 if (!is_dir($dir)) return; 794 795 $items = scandir($dir); 796 foreach ($items as $item) { 797 if ($item === '.' || $item === '..' || $item === 'calendar') continue; 798 799 $path = $dir . $item; 800 if (is_dir($path)) { 801 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 802 $namespaces[] = $namespace; 803 $this->scanForNamespaces($path . '/', $namespace, $namespaces); 804 } 805 } 806 } 807} 808