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