xref: /plugin/calendar/classes/EventManager.php (revision 2866e8271e4daef3b32eacb3a9082d02159b592b)
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