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