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