xref: /plugin/calendar/action.php (revision 87ac9bf3391b3f7059f4ccd6abc619e9db5fad8d)
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        $description = $INPUT->str('description', '');
55        $color = $INPUT->str('color', '#3498db');
56        $oldDate = $INPUT->str('oldDate', ''); // Track original date for moves
57        $isTask = $INPUT->bool('isTask', false);
58        $completed = $INPUT->bool('completed', false);
59        $endDate = $INPUT->str('endDate', '');
60        $isRecurring = $INPUT->bool('isRecurring', false);
61        $recurrenceType = $INPUT->str('recurrenceType', 'weekly');
62        $recurrenceEnd = $INPUT->str('recurrenceEnd', '');
63
64        if (!$date || !$title) {
65            echo json_encode(['success' => false, 'error' => 'Missing required fields']);
66            return;
67        }
68
69        // Generate event ID if new
70        $generatedId = $eventId ?: uniqid();
71
72        // If recurring, generate multiple events
73        if ($isRecurring) {
74            $this->createRecurringEvents($namespace, $date, $endDate, $title, $time, $description,
75                                        $color, $isTask, $recurrenceType, $recurrenceEnd, $generatedId);
76            echo json_encode(['success' => true]);
77            return;
78        }
79
80        list($year, $month, $day) = explode('-', $date);
81
82        $dataDir = DOKU_INC . 'data/meta/';
83        if ($namespace) {
84            $dataDir .= str_replace(':', '/', $namespace) . '/';
85        }
86        $dataDir .= 'calendar/';
87
88        if (!is_dir($dataDir)) {
89            mkdir($dataDir, 0755, true);
90        }
91
92        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
93
94        $events = [];
95        if (file_exists($eventFile)) {
96            $events = json_decode(file_get_contents($eventFile), true);
97        }
98
99        // If editing and date changed, remove from old date first
100        if ($eventId && $oldDate && $oldDate !== $date) {
101            list($oldYear, $oldMonth, $oldDay) = explode('-', $oldDate);
102            $oldEventFile = $dataDir . sprintf('%04d-%02d.json', $oldYear, $oldMonth);
103
104            if (file_exists($oldEventFile)) {
105                $oldEvents = json_decode(file_get_contents($oldEventFile), true);
106                if (isset($oldEvents[$oldDate])) {
107                    $oldEvents[$oldDate] = array_filter($oldEvents[$oldDate], function($evt) use ($eventId) {
108                        return $evt['id'] !== $eventId;
109                    });
110
111                    if (empty($oldEvents[$oldDate])) {
112                        unset($oldEvents[$oldDate]);
113                    }
114
115                    file_put_contents($oldEventFile, json_encode($oldEvents, JSON_PRETTY_PRINT));
116                }
117            }
118        }
119
120        if (!isset($events[$date])) {
121            $events[$date] = [];
122        }
123
124        $eventData = [
125            'id' => $generatedId,
126            'title' => $title,
127            'time' => $time,
128            'description' => $description,
129            'color' => $color,
130            'isTask' => $isTask,
131            'completed' => $completed,
132            'endDate' => $endDate,
133            'created' => date('Y-m-d H:i:s')
134        ];
135
136        // If editing, replace existing event
137        if ($eventId) {
138            $found = false;
139            foreach ($events[$date] as $key => $evt) {
140                if ($evt['id'] === $eventId) {
141                    $events[$date][$key] = $eventData;
142                    $found = true;
143                    break;
144                }
145            }
146            if (!$found) {
147                $events[$date][] = $eventData;
148            }
149        } else {
150            $events[$date][] = $eventData;
151        }
152
153        file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT));
154
155        echo json_encode(['success' => true, 'events' => $events, 'eventId' => $eventData['id']]);
156    }
157
158    private function deleteEvent() {
159        global $INPUT;
160
161        $namespace = $INPUT->str('namespace', '');
162        $date = $INPUT->str('date');
163        $eventId = $INPUT->str('eventId');
164
165        list($year, $month, $day) = explode('-', $date);
166
167        $dataDir = DOKU_INC . 'data/meta/';
168        if ($namespace) {
169            $dataDir .= str_replace(':', '/', $namespace) . '/';
170        }
171        $dataDir .= 'calendar/';
172
173        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
174
175        if (file_exists($eventFile)) {
176            $events = json_decode(file_get_contents($eventFile), true);
177
178            if (isset($events[$date])) {
179                $events[$date] = array_filter($events[$date], function($event) use ($eventId) {
180                    return $event['id'] !== $eventId;
181                });
182
183                if (empty($events[$date])) {
184                    unset($events[$date]);
185                }
186
187                file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT));
188            }
189        }
190
191        echo json_encode(['success' => true]);
192    }
193
194    private function getEvent() {
195        global $INPUT;
196
197        $namespace = $INPUT->str('namespace', '');
198        $date = $INPUT->str('date');
199        $eventId = $INPUT->str('eventId');
200
201        list($year, $month, $day) = explode('-', $date);
202
203        $dataDir = DOKU_INC . 'data/meta/';
204        if ($namespace) {
205            $dataDir .= str_replace(':', '/', $namespace) . '/';
206        }
207        $dataDir .= 'calendar/';
208
209        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
210
211        if (file_exists($eventFile)) {
212            $events = json_decode(file_get_contents($eventFile), true);
213
214            if (isset($events[$date])) {
215                foreach ($events[$date] as $event) {
216                    if ($event['id'] === $eventId) {
217                        echo json_encode(['success' => true, 'event' => $event]);
218                        return;
219                    }
220                }
221            }
222        }
223
224        echo json_encode(['success' => false, 'error' => 'Event not found']);
225    }
226
227    private function loadMonth() {
228        global $INPUT;
229
230        $namespace = $INPUT->str('namespace', '');
231        $year = $INPUT->int('year');
232        $month = $INPUT->int('month');
233
234        $dataDir = DOKU_INC . 'data/meta/';
235        if ($namespace) {
236            $dataDir .= str_replace(':', '/', $namespace) . '/';
237        }
238        $dataDir .= 'calendar/';
239
240        error_log("Calendar loadMonth: Loading $year-$month");
241
242        // Load current month
243        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
244        $events = [];
245        if (file_exists($eventFile)) {
246            $contents = file_get_contents($eventFile);
247            $decoded = json_decode($contents, true);
248            if (json_last_error() === JSON_ERROR_NONE) {
249                $events = $decoded;
250                error_log("Calendar loadMonth: Loaded " . count($events) . " dates from $eventFile");
251            } else {
252                error_log('Calendar: JSON decode error in ' . $eventFile . ': ' . json_last_error_msg());
253            }
254        } else {
255            error_log("Calendar loadMonth: File not found: $eventFile");
256        }
257
258        // Load previous month to catch events spanning into current month
259        $prevMonth = $month - 1;
260        $prevYear = $year;
261        if ($prevMonth < 1) {
262            $prevMonth = 12;
263            $prevYear--;
264        }
265        $prevEventFile = $dataDir . sprintf('%04d-%02d.json', $prevYear, $prevMonth);
266        if (file_exists($prevEventFile)) {
267            $contents = file_get_contents($prevEventFile);
268            $decoded = json_decode($contents, true);
269            if (json_last_error() === JSON_ERROR_NONE) {
270                error_log("Calendar loadMonth: Loaded " . count($decoded) . " dates from $prevEventFile");
271                $events = array_merge($events, $decoded);
272            } else {
273                error_log('Calendar: JSON decode error in ' . $prevEventFile . ': ' . json_last_error_msg());
274            }
275        }
276
277        // Load next month to catch events spanning from current month
278        $nextMonth = $month + 1;
279        $nextYear = $year;
280        if ($nextMonth > 12) {
281            $nextMonth = 1;
282            $nextYear++;
283        }
284        $nextEventFile = $dataDir . sprintf('%04d-%02d.json', $nextYear, $nextMonth);
285        if (file_exists($nextEventFile)) {
286            $contents = file_get_contents($nextEventFile);
287            $decoded = json_decode($contents, true);
288            if (json_last_error() === JSON_ERROR_NONE) {
289                error_log("Calendar loadMonth: Loaded " . count($decoded) . " dates from $nextEventFile");
290                $events = array_merge($events, $decoded);
291            } else {
292                error_log('Calendar: JSON decode error in ' . $nextEventFile . ': ' . json_last_error_msg());
293            }
294        }
295
296        error_log("Calendar loadMonth: Total dates returned: " . count($events));
297
298        echo json_encode(['success' => true, 'events' => $events, 'year' => $year, 'month' => $month]);
299    }
300
301    private function toggleTaskComplete() {
302        global $INPUT;
303
304        $namespace = $INPUT->str('namespace', '');
305        $date = $INPUT->str('date');
306        $eventId = $INPUT->str('eventId');
307        $completed = $INPUT->bool('completed', false);
308
309        list($year, $month, $day) = explode('-', $date);
310
311        $dataDir = DOKU_INC . 'data/meta/';
312        if ($namespace) {
313            $dataDir .= str_replace(':', '/', $namespace) . '/';
314        }
315        $dataDir .= 'calendar/';
316
317        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
318
319        if (file_exists($eventFile)) {
320            $events = json_decode(file_get_contents($eventFile), true);
321
322            if (isset($events[$date])) {
323                foreach ($events[$date] as $key => $event) {
324                    if ($event['id'] === $eventId) {
325                        $events[$date][$key]['completed'] = $completed;
326                        break;
327                    }
328                }
329
330                file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT));
331                echo json_encode(['success' => true, 'events' => $events]);
332                return;
333            }
334        }
335
336        echo json_encode(['success' => false, 'error' => 'Event not found']);
337    }
338
339    private function createRecurringEvents($namespace, $startDate, $endDate, $title, $time,
340                                          $description, $color, $isTask, $recurrenceType,
341                                          $recurrenceEnd, $baseId) {
342        $dataDir = DOKU_INC . 'data/meta/';
343        if ($namespace) {
344            $dataDir .= str_replace(':', '/', $namespace) . '/';
345        }
346        $dataDir .= 'calendar/';
347
348        if (!is_dir($dataDir)) {
349            mkdir($dataDir, 0755, true);
350        }
351
352        // Calculate recurrence interval
353        $interval = '';
354        switch ($recurrenceType) {
355            case 'daily': $interval = '+1 day'; break;
356            case 'weekly': $interval = '+1 week'; break;
357            case 'monthly': $interval = '+1 month'; break;
358            case 'yearly': $interval = '+1 year'; break;
359            default: $interval = '+1 week';
360        }
361
362        // Set maximum end date if not specified (1 year from start)
363        $maxEnd = $recurrenceEnd ?: date('Y-m-d', strtotime($startDate . ' +1 year'));
364
365        // Calculate event duration for multi-day events
366        $eventDuration = 0;
367        if ($endDate && $endDate !== $startDate) {
368            $start = new DateTime($startDate);
369            $end = new DateTime($endDate);
370            $eventDuration = $start->diff($end)->days;
371        }
372
373        // Generate recurring events
374        $currentDate = new DateTime($startDate);
375        $endLimit = new DateTime($maxEnd);
376        $counter = 0;
377        $maxOccurrences = 100; // Prevent infinite loops
378
379        while ($currentDate <= $endLimit && $counter < $maxOccurrences) {
380            $dateKey = $currentDate->format('Y-m-d');
381            list($year, $month, $day) = explode('-', $dateKey);
382
383            // Calculate end date for this occurrence if multi-day
384            $occurrenceEndDate = '';
385            if ($eventDuration > 0) {
386                $occurrenceEnd = clone $currentDate;
387                $occurrenceEnd->modify('+' . $eventDuration . ' days');
388                $occurrenceEndDate = $occurrenceEnd->format('Y-m-d');
389            }
390
391            // Load month file
392            $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
393            $events = [];
394            if (file_exists($eventFile)) {
395                $events = json_decode(file_get_contents($eventFile), true);
396            }
397
398            if (!isset($events[$dateKey])) {
399                $events[$dateKey] = [];
400            }
401
402            // Create event for this occurrence
403            $eventData = [
404                'id' => $baseId . '-' . $counter,
405                'title' => $title,
406                'time' => $time,
407                'description' => $description,
408                'color' => $color,
409                'isTask' => $isTask,
410                'completed' => false,
411                'endDate' => $occurrenceEndDate,
412                'recurring' => true,
413                'recurringId' => $baseId,
414                'created' => date('Y-m-d H:i:s')
415            ];
416
417            $events[$dateKey][] = $eventData;
418            file_put_contents($eventFile, json_encode($events, JSON_PRETTY_PRINT));
419
420            // Move to next occurrence
421            $currentDate->modify($interval);
422            $counter++;
423        }
424    }
425
426    public function addAssets(Doku_Event $event, $param) {
427        $event->data['link'][] = array(
428            'type' => 'text/css',
429            'rel' => 'stylesheet',
430            'href' => DOKU_BASE . 'lib/plugins/calendar/style.css'
431        );
432
433        $event->data['script'][] = array(
434            'type' => 'text/javascript',
435            'src' => DOKU_BASE . 'lib/plugins/calendar/script.js'
436        );
437    }
438}
439