1815440faSAtari911<?php 2815440faSAtari911/** 3815440faSAtari911 * Calendar Plugin - Event Manager 4815440faSAtari911 * 5815440faSAtari911 * Consolidates event CRUD operations with proper file locking and caching. 6815440faSAtari911 * This class is the single point of entry for all event data operations. 7815440faSAtari911 * 8815440faSAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 9815440faSAtari911 * @author DokuWiki Community 10*2866e827SAtari911 * @version 7.2.6 11815440faSAtari911 */ 12815440faSAtari911 13815440faSAtari911if (!defined('DOKU_INC')) die(); 14815440faSAtari911 15815440faSAtari911// Require dependencies 16815440faSAtari911require_once __DIR__ . '/FileHandler.php'; 17815440faSAtari911require_once __DIR__ . '/EventCache.php'; 18815440faSAtari911 19815440faSAtari911class CalendarEventManager { 20815440faSAtari911 21815440faSAtari911 /** @var string Base data directory */ 22815440faSAtari911 private static $baseDir = null; 23815440faSAtari911 24815440faSAtari911 /** 25815440faSAtari911 * Get the base calendar data directory 26815440faSAtari911 * 27815440faSAtari911 * @return string Base directory path 28815440faSAtari911 */ 29815440faSAtari911 private static function getBaseDir() { 30815440faSAtari911 if (self::$baseDir === null) { 31*2866e827SAtari911 global $conf; 32*2866e827SAtari911 self::$baseDir = rtrim($conf['metadir'], '/') . '/'; 33815440faSAtari911 } 34815440faSAtari911 return self::$baseDir; 35815440faSAtari911 } 36815440faSAtari911 37815440faSAtari911 /** 38815440faSAtari911 * Get the data directory for a namespace 39815440faSAtari911 * 40815440faSAtari911 * @param string $namespace Namespace (empty for default) 41815440faSAtari911 * @return string Directory path 42815440faSAtari911 */ 43815440faSAtari911 private static function getNamespaceDir($namespace = '') { 44815440faSAtari911 $dir = self::getBaseDir(); 45815440faSAtari911 if ($namespace) { 46815440faSAtari911 $dir .= str_replace(':', '/', $namespace) . '/'; 47815440faSAtari911 } 48815440faSAtari911 $dir .= 'calendar/'; 49815440faSAtari911 return $dir; 50815440faSAtari911 } 51815440faSAtari911 52815440faSAtari911 /** 53815440faSAtari911 * Get the event file path for a specific month 54815440faSAtari911 * 55815440faSAtari911 * @param string $namespace Namespace 56815440faSAtari911 * @param int $year Year 57815440faSAtari911 * @param int $month Month 58815440faSAtari911 * @return string File path 59815440faSAtari911 */ 60815440faSAtari911 private static function getEventFile($namespace, $year, $month) { 61815440faSAtari911 $dir = self::getNamespaceDir($namespace); 62815440faSAtari911 return $dir . sprintf('%04d-%02d.json', $year, $month); 63815440faSAtari911 } 64815440faSAtari911 65815440faSAtari911 /** 66815440faSAtari911 * Load events for a specific month 67815440faSAtari911 * 68815440faSAtari911 * @param string $namespace Namespace filter 69815440faSAtari911 * @param int $year Year 70815440faSAtari911 * @param int $month Month 71815440faSAtari911 * @param bool $useCache Whether to use caching 72815440faSAtari911 * @return array Events indexed by date 73815440faSAtari911 */ 74815440faSAtari911 public static function loadMonth($namespace, $year, $month, $useCache = true) { 75815440faSAtari911 // Check cache first 76815440faSAtari911 if ($useCache) { 77815440faSAtari911 $cached = CalendarEventCache::getMonthEvents($namespace, $year, $month); 78815440faSAtari911 if ($cached !== null) { 79815440faSAtari911 return $cached; 80815440faSAtari911 } 81815440faSAtari911 } 82815440faSAtari911 83815440faSAtari911 $events = []; 84815440faSAtari911 85815440faSAtari911 // Handle wildcards and multiple namespaces 86815440faSAtari911 if (strpos($namespace, '*') !== false || strpos($namespace, ';') !== false) { 87815440faSAtari911 $events = self::loadMonthMultiNamespace($namespace, $year, $month); 88815440faSAtari911 } else { 89815440faSAtari911 $eventFile = self::getEventFile($namespace, $year, $month); 90815440faSAtari911 $events = CalendarFileHandler::readJson($eventFile); 91815440faSAtari911 } 92815440faSAtari911 93815440faSAtari911 // Store in cache 94815440faSAtari911 if ($useCache) { 95815440faSAtari911 CalendarEventCache::setMonthEvents($namespace, $year, $month, $events); 96815440faSAtari911 } 97815440faSAtari911 98815440faSAtari911 return $events; 99815440faSAtari911 } 100815440faSAtari911 101815440faSAtari911 /** 102815440faSAtari911 * Load events from multiple namespaces 103815440faSAtari911 * 104815440faSAtari911 * @param string $namespacePattern Namespace pattern (with * or ;) 105815440faSAtari911 * @param int $year Year 106815440faSAtari911 * @param int $month Month 107815440faSAtari911 * @return array Merged events indexed by date 108815440faSAtari911 */ 109815440faSAtari911 private static function loadMonthMultiNamespace($namespacePattern, $year, $month) { 110815440faSAtari911 $allEvents = []; 111815440faSAtari911 $namespaces = self::expandNamespacePattern($namespacePattern); 112815440faSAtari911 113815440faSAtari911 foreach ($namespaces as $ns) { 114815440faSAtari911 $eventFile = self::getEventFile($ns, $year, $month); 115815440faSAtari911 $events = CalendarFileHandler::readJson($eventFile); 116815440faSAtari911 117815440faSAtari911 foreach ($events as $date => $dateEvents) { 118815440faSAtari911 if (!isset($allEvents[$date])) { 119815440faSAtari911 $allEvents[$date] = []; 120815440faSAtari911 } 121815440faSAtari911 foreach ($dateEvents as $event) { 122815440faSAtari911 // Ensure namespace is set 123815440faSAtari911 if (!isset($event['namespace'])) { 124815440faSAtari911 $event['namespace'] = $ns; 125815440faSAtari911 } 126815440faSAtari911 $allEvents[$date][] = $event; 127815440faSAtari911 } 128815440faSAtari911 } 129815440faSAtari911 } 130815440faSAtari911 131815440faSAtari911 return $allEvents; 132815440faSAtari911 } 133815440faSAtari911 134815440faSAtari911 /** 135815440faSAtari911 * Expand namespace pattern to list of namespaces 136815440faSAtari911 * 137815440faSAtari911 * @param string $pattern Namespace pattern 138815440faSAtari911 * @return array List of namespace paths 139815440faSAtari911 */ 140815440faSAtari911 private static function expandNamespacePattern($pattern) { 141815440faSAtari911 $namespaces = []; 142815440faSAtari911 143815440faSAtari911 // Handle semicolon-separated namespaces 144815440faSAtari911 if (strpos($pattern, ';') !== false) { 145815440faSAtari911 $parts = explode(';', $pattern); 146815440faSAtari911 foreach ($parts as $part) { 147815440faSAtari911 $expanded = self::expandNamespacePattern(trim($part)); 148815440faSAtari911 $namespaces = array_merge($namespaces, $expanded); 149815440faSAtari911 } 150815440faSAtari911 return array_unique($namespaces); 151815440faSAtari911 } 152815440faSAtari911 153815440faSAtari911 // Handle wildcard 154815440faSAtari911 if (strpos($pattern, '*') !== false) { 155815440faSAtari911 // Get base directory 156815440faSAtari911 $basePattern = str_replace('*', '', $pattern); 157815440faSAtari911 $basePattern = rtrim($basePattern, ':'); 158815440faSAtari911 159815440faSAtari911 $searchDir = self::getBaseDir(); 160815440faSAtari911 if ($basePattern) { 161815440faSAtari911 $searchDir .= str_replace(':', '/', $basePattern) . '/'; 162815440faSAtari911 } 163815440faSAtari911 164815440faSAtari911 // Always include the base namespace 165815440faSAtari911 $namespaces[] = $basePattern; 166815440faSAtari911 167815440faSAtari911 // Find subdirectories with calendar data 168815440faSAtari911 if (is_dir($searchDir)) { 169815440faSAtari911 $iterator = new RecursiveIteratorIterator( 170815440faSAtari911 new RecursiveDirectoryIterator($searchDir, RecursiveDirectoryIterator::SKIP_DOTS), 171815440faSAtari911 RecursiveIteratorIterator::SELF_FIRST 172815440faSAtari911 ); 173815440faSAtari911 174815440faSAtari911 foreach ($iterator as $file) { 175815440faSAtari911 if ($file->isDir() && $file->getFilename() === 'calendar') { 176815440faSAtari911 // Extract namespace from path 177815440faSAtari911 $path = dirname($file->getPathname()); 178815440faSAtari911 $relPath = str_replace(self::getBaseDir(), '', $path); 179815440faSAtari911 $ns = str_replace('/', ':', trim($relPath, '/')); 180815440faSAtari911 if ($ns && !in_array($ns, $namespaces)) { 181815440faSAtari911 $namespaces[] = $ns; 182815440faSAtari911 } 183815440faSAtari911 } 184815440faSAtari911 } 185815440faSAtari911 } 186815440faSAtari911 187815440faSAtari911 return $namespaces; 188815440faSAtari911 } 189815440faSAtari911 190815440faSAtari911 // Simple namespace 191815440faSAtari911 return [$pattern]; 192815440faSAtari911 } 193815440faSAtari911 194815440faSAtari911 /** 195815440faSAtari911 * Save an event 196815440faSAtari911 * 197815440faSAtari911 * @param array $eventData Event data 198815440faSAtari911 * @param string|null $oldDate Previous date (for moves) 199815440faSAtari911 * @param string|null $oldNamespace Previous namespace (for moves) 200815440faSAtari911 * @return array Result with success status and event data 201815440faSAtari911 */ 202815440faSAtari911 public static function saveEvent(array $eventData, $oldDate = null, $oldNamespace = null) { 203815440faSAtari911 // Validate required fields 204815440faSAtari911 if (empty($eventData['date']) || empty($eventData['title'])) { 205815440faSAtari911 return ['success' => false, 'error' => 'Missing required fields']; 206815440faSAtari911 } 207815440faSAtari911 208815440faSAtari911 $date = $eventData['date']; 209815440faSAtari911 $namespace = $eventData['namespace'] ?? ''; 210815440faSAtari911 $eventId = $eventData['id'] ?? uniqid(); 211815440faSAtari911 212815440faSAtari911 // Parse date 213815440faSAtari911 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches)) { 214815440faSAtari911 return ['success' => false, 'error' => 'Invalid date format']; 215815440faSAtari911 } 216815440faSAtari911 list(, $year, $month, $day) = $matches; 217815440faSAtari911 $year = (int)$year; 218815440faSAtari911 $month = (int)$month; 219815440faSAtari911 220815440faSAtari911 // Ensure ID is set 221815440faSAtari911 $eventData['id'] = $eventId; 222815440faSAtari911 223815440faSAtari911 // Set created timestamp if new 224815440faSAtari911 if (!isset($eventData['created'])) { 225815440faSAtari911 $eventData['created'] = date('Y-m-d H:i:s'); 226815440faSAtari911 } 227815440faSAtari911 228815440faSAtari911 // Handle event move (different date or namespace) 229815440faSAtari911 $dateChanged = $oldDate && $oldDate !== $date; 230815440faSAtari911 $namespaceChanged = $oldNamespace !== null && $oldNamespace !== $namespace; 231815440faSAtari911 232815440faSAtari911 if ($dateChanged || $namespaceChanged) { 233815440faSAtari911 // Delete from old location 234815440faSAtari911 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $oldDate ?: $date, $oldMatches)) { 235815440faSAtari911 return ['success' => false, 'error' => 'Invalid old date format']; 236815440faSAtari911 } 237815440faSAtari911 list(, $oldYear, $oldMonth, $oldDay) = $oldMatches; 238815440faSAtari911 239815440faSAtari911 $oldEventFile = self::getEventFile($oldNamespace ?? $namespace, (int)$oldYear, (int)$oldMonth); 240815440faSAtari911 $oldEvents = CalendarFileHandler::readJson($oldEventFile); 241815440faSAtari911 242815440faSAtari911 $deleteDate = $oldDate ?: $date; 243815440faSAtari911 if (isset($oldEvents[$deleteDate])) { 244815440faSAtari911 $oldEvents[$deleteDate] = array_values(array_filter( 245815440faSAtari911 $oldEvents[$deleteDate], 246815440faSAtari911 function($evt) use ($eventId) { 247815440faSAtari911 return $evt['id'] !== $eventId; 248815440faSAtari911 } 249815440faSAtari911 )); 250815440faSAtari911 251815440faSAtari911 if (empty($oldEvents[$deleteDate])) { 252815440faSAtari911 unset($oldEvents[$deleteDate]); 253815440faSAtari911 } 254815440faSAtari911 255815440faSAtari911 CalendarFileHandler::writeJson($oldEventFile, $oldEvents); 256815440faSAtari911 257815440faSAtari911 // Invalidate old location cache 258815440faSAtari911 CalendarEventCache::invalidateMonth($oldNamespace ?? $namespace, (int)$oldYear, (int)$oldMonth); 259815440faSAtari911 } 260815440faSAtari911 } 261815440faSAtari911 262815440faSAtari911 // Load current events 263815440faSAtari911 $eventFile = self::getEventFile($namespace, $year, $month); 264815440faSAtari911 $events = CalendarFileHandler::readJson($eventFile); 265815440faSAtari911 266815440faSAtari911 // Ensure date array exists 267815440faSAtari911 if (!isset($events[$date]) || !is_array($events[$date])) { 268815440faSAtari911 $events[$date] = []; 269815440faSAtari911 } 270815440faSAtari911 271815440faSAtari911 // Update or add event 272815440faSAtari911 $found = false; 273815440faSAtari911 foreach ($events[$date] as $key => $evt) { 274815440faSAtari911 if ($evt['id'] === $eventId) { 275815440faSAtari911 $events[$date][$key] = $eventData; 276815440faSAtari911 $found = true; 277815440faSAtari911 break; 278815440faSAtari911 } 279815440faSAtari911 } 280815440faSAtari911 281815440faSAtari911 if (!$found) { 282815440faSAtari911 $events[$date][] = $eventData; 283815440faSAtari911 } 284815440faSAtari911 285815440faSAtari911 // Save with atomic write 286815440faSAtari911 if (!CalendarFileHandler::writeJson($eventFile, $events)) { 287815440faSAtari911 return ['success' => false, 'error' => 'Failed to save event']; 288815440faSAtari911 } 289815440faSAtari911 290815440faSAtari911 // Invalidate cache 291815440faSAtari911 CalendarEventCache::invalidateMonth($namespace, $year, $month); 292815440faSAtari911 293815440faSAtari911 return ['success' => true, 'event' => $eventData]; 294815440faSAtari911 } 295815440faSAtari911 296815440faSAtari911 /** 297815440faSAtari911 * Delete an event 298815440faSAtari911 * 299815440faSAtari911 * @param string $eventId Event ID 300815440faSAtari911 * @param string $date Event date 301815440faSAtari911 * @param string $namespace Namespace 302815440faSAtari911 * @return array Result with success status 303815440faSAtari911 */ 304815440faSAtari911 public static function deleteEvent($eventId, $date, $namespace = '') { 305815440faSAtari911 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches)) { 306815440faSAtari911 return ['success' => false, 'error' => 'Invalid date format']; 307815440faSAtari911 } 308815440faSAtari911 list(, $year, $month, $day) = $matches; 309815440faSAtari911 $year = (int)$year; 310815440faSAtari911 $month = (int)$month; 311815440faSAtari911 312815440faSAtari911 $eventFile = self::getEventFile($namespace, $year, $month); 313815440faSAtari911 $events = CalendarFileHandler::readJson($eventFile); 314815440faSAtari911 315815440faSAtari911 if (!isset($events[$date])) { 316815440faSAtari911 return ['success' => false, 'error' => 'Event not found']; 317815440faSAtari911 } 318815440faSAtari911 319815440faSAtari911 $originalCount = count($events[$date]); 320815440faSAtari911 $events[$date] = array_values(array_filter( 321815440faSAtari911 $events[$date], 322815440faSAtari911 function($evt) use ($eventId) { 323815440faSAtari911 return $evt['id'] !== $eventId; 324815440faSAtari911 } 325815440faSAtari911 )); 326815440faSAtari911 327815440faSAtari911 if (count($events[$date]) === $originalCount) { 328815440faSAtari911 return ['success' => false, 'error' => 'Event not found']; 329815440faSAtari911 } 330815440faSAtari911 331815440faSAtari911 if (empty($events[$date])) { 332815440faSAtari911 unset($events[$date]); 333815440faSAtari911 } 334815440faSAtari911 335815440faSAtari911 if (!CalendarFileHandler::writeJson($eventFile, $events)) { 336815440faSAtari911 return ['success' => false, 'error' => 'Failed to delete event']; 337815440faSAtari911 } 338815440faSAtari911 339815440faSAtari911 // Invalidate cache 340815440faSAtari911 CalendarEventCache::invalidateMonth($namespace, $year, $month); 341815440faSAtari911 342815440faSAtari911 return ['success' => true]; 343815440faSAtari911 } 344815440faSAtari911 345815440faSAtari911 /** 346815440faSAtari911 * Get a single event by ID 347815440faSAtari911 * 348815440faSAtari911 * @param string $eventId Event ID 349815440faSAtari911 * @param string $date Event date 350815440faSAtari911 * @param string $namespace Namespace (use * for all) 351815440faSAtari911 * @return array|null Event data or null if not found 352815440faSAtari911 */ 353815440faSAtari911 public static function getEvent($eventId, $date, $namespace = '') { 354815440faSAtari911 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches)) { 355815440faSAtari911 return null; 356815440faSAtari911 } 357815440faSAtari911 list(, $year, $month, $day) = $matches; 358815440faSAtari911 359815440faSAtari911 $events = self::loadMonth($namespace, (int)$year, (int)$month); 360815440faSAtari911 361815440faSAtari911 if (!isset($events[$date])) { 362815440faSAtari911 return null; 363815440faSAtari911 } 364815440faSAtari911 365815440faSAtari911 foreach ($events[$date] as $event) { 366815440faSAtari911 if ($event['id'] === $eventId) { 367815440faSAtari911 return $event; 368815440faSAtari911 } 369815440faSAtari911 } 370815440faSAtari911 371815440faSAtari911 return null; 372815440faSAtari911 } 373815440faSAtari911 374815440faSAtari911 /** 375815440faSAtari911 * Find which namespace an event is in 376815440faSAtari911 * 377815440faSAtari911 * @param string $eventId Event ID 378815440faSAtari911 * @param string $date Event date 379815440faSAtari911 * @return string|null Namespace or null if not found 380815440faSAtari911 */ 381815440faSAtari911 public static function findEventNamespace($eventId, $date) { 382815440faSAtari911 if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $matches)) { 383815440faSAtari911 return null; 384815440faSAtari911 } 385815440faSAtari911 list(, $year, $month, $day) = $matches; 386815440faSAtari911 387815440faSAtari911 // Load all namespaces 388815440faSAtari911 $events = self::loadMonth('*', (int)$year, (int)$month, false); 389815440faSAtari911 390815440faSAtari911 if (!isset($events[$date])) { 391815440faSAtari911 return null; 392815440faSAtari911 } 393815440faSAtari911 394815440faSAtari911 foreach ($events[$date] as $event) { 395815440faSAtari911 if ($event['id'] === $eventId) { 396815440faSAtari911 return $event['namespace'] ?? ''; 397815440faSAtari911 } 398815440faSAtari911 } 399815440faSAtari911 400815440faSAtari911 return null; 401815440faSAtari911 } 402815440faSAtari911 403815440faSAtari911 /** 404815440faSAtari911 * Search events across all namespaces 405815440faSAtari911 * 406815440faSAtari911 * @param string $query Search query 407815440faSAtari911 * @param array $options Search options (dateFrom, dateTo, namespace) 408815440faSAtari911 * @return array Matching events 409815440faSAtari911 */ 410815440faSAtari911 public static function searchEvents($query, array $options = []) { 411815440faSAtari911 $results = []; 412815440faSAtari911 $query = strtolower(trim($query)); 413815440faSAtari911 414815440faSAtari911 $namespace = $options['namespace'] ?? '*'; 415815440faSAtari911 $dateFrom = $options['dateFrom'] ?? date('Y-m-01'); 416815440faSAtari911 $dateTo = $options['dateTo'] ?? date('Y-m-d', strtotime('+1 year')); 417815440faSAtari911 418815440faSAtari911 // Parse date range 419815440faSAtari911 $startDate = new DateTime($dateFrom); 420815440faSAtari911 $endDate = new DateTime($dateTo); 421815440faSAtari911 422815440faSAtari911 // Iterate through months 423815440faSAtari911 $current = clone $startDate; 424815440faSAtari911 $current->modify('first day of this month'); 425815440faSAtari911 426815440faSAtari911 while ($current <= $endDate) { 427815440faSAtari911 $year = (int)$current->format('Y'); 428815440faSAtari911 $month = (int)$current->format('m'); 429815440faSAtari911 430815440faSAtari911 $events = self::loadMonth($namespace, $year, $month); 431815440faSAtari911 432815440faSAtari911 foreach ($events as $date => $dateEvents) { 433815440faSAtari911 if ($date < $dateFrom || $date > $dateTo) { 434815440faSAtari911 continue; 435815440faSAtari911 } 436815440faSAtari911 437815440faSAtari911 foreach ($dateEvents as $event) { 438815440faSAtari911 $titleMatch = stripos($event['title'] ?? '', $query) !== false; 439815440faSAtari911 $descMatch = stripos($event['description'] ?? '', $query) !== false; 440815440faSAtari911 441815440faSAtari911 if ($titleMatch || $descMatch) { 442815440faSAtari911 $event['_date'] = $date; 443815440faSAtari911 $results[] = $event; 444815440faSAtari911 } 445815440faSAtari911 } 446815440faSAtari911 } 447815440faSAtari911 448815440faSAtari911 $current->modify('+1 month'); 449815440faSAtari911 } 450815440faSAtari911 451815440faSAtari911 // Sort by date 452815440faSAtari911 usort($results, function($a, $b) { 453815440faSAtari911 return strcmp($a['_date'], $b['_date']); 454815440faSAtari911 }); 455815440faSAtari911 456815440faSAtari911 return $results; 457815440faSAtari911 } 458815440faSAtari911 459815440faSAtari911 /** 460815440faSAtari911 * Get all namespaces that have calendar data 461815440faSAtari911 * 462815440faSAtari911 * @return array List of namespaces 463815440faSAtari911 */ 464815440faSAtari911 public static function getNamespaces() { 465815440faSAtari911 return self::expandNamespacePattern('*'); 466815440faSAtari911 } 467815440faSAtari911 468815440faSAtari911 /** 469815440faSAtari911 * Debug log helper 470815440faSAtari911 * 471815440faSAtari911 * @param string $message Message to log 472815440faSAtari911 */ 473815440faSAtari911 private static function log($message) { 474815440faSAtari911 if (defined('CALENDAR_DEBUG') && CALENDAR_DEBUG) { 475815440faSAtari911 error_log("[Calendar EventManager] $message"); 476815440faSAtari911 } 477815440faSAtari911 } 478815440faSAtari911} 479