xref: /plugin/calendar/classes/EventCache.php (revision 2866e8271e4daef3b32eacb3a9082d02159b592b)
1<?php
2/**
3 * Calendar Plugin - Event Cache
4 *
5 * Provides caching for calendar events to avoid reloading JSON files
6 * on every page view. Uses file-based caching with TTL.
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 * @author  DokuWiki Community
10 * @version 7.2.6
11 */
12
13if (!defined('DOKU_INC')) die();
14
15class CalendarEventCache {
16
17    /** @var int Default cache TTL in seconds (5 minutes) */
18    private const DEFAULT_TTL = 300;
19
20    /** @var string Cache directory relative to DokuWiki data */
21    private const CACHE_DIR = 'cache/calendar/';
22
23    /** @var array In-memory cache for current request */
24    private static $memoryCache = [];
25
26    /** @var string|null Base cache directory path */
27    private static $cacheDir = null;
28
29    /**
30     * Get the cache directory path
31     *
32     * @return string Cache directory path
33     */
34    private static function getCacheDir() {
35        if (self::$cacheDir === null) {
36            global $conf;
37            self::$cacheDir = rtrim($conf['cachedir'], '/') . '/calendar/';
38            if (!is_dir(self::$cacheDir)) {
39                @mkdir(self::$cacheDir, 0755, true);
40            }
41        }
42        return self::$cacheDir;
43    }
44
45    /**
46     * Generate cache key for a month's events
47     *
48     * @param string $namespace Namespace filter
49     * @param int $year Year
50     * @param int $month Month
51     * @return string Cache key
52     */
53    private static function getMonthCacheKey($namespace, $year, $month) {
54        $ns = preg_replace('/[^a-zA-Z0-9_-]/', '_', $namespace ?: 'default');
55        return sprintf('events_%s_%04d_%02d', $ns, $year, $month);
56    }
57
58    /**
59     * Get cache file path
60     *
61     * @param string $key Cache key
62     * @return string File path
63     */
64    private static function getCacheFile($key) {
65        return self::getCacheDir() . $key . '.cache';
66    }
67
68    /**
69     * Get cached events for a month
70     *
71     * @param string $namespace Namespace filter
72     * @param int $year Year
73     * @param int $month Month
74     * @param int $ttl TTL in seconds (default 5 minutes)
75     * @return array|null Cached events or null if cache miss/expired
76     */
77    public static function getMonthEvents($namespace, $year, $month, $ttl = self::DEFAULT_TTL) {
78        $key = self::getMonthCacheKey($namespace, $year, $month);
79
80        // Check memory cache first
81        if (isset(self::$memoryCache[$key])) {
82            return self::$memoryCache[$key];
83        }
84
85        $cacheFile = self::getCacheFile($key);
86
87        if (!file_exists($cacheFile)) {
88            return null;
89        }
90
91        // Check if cache is expired
92        $mtime = filemtime($cacheFile);
93        if ($mtime === false || (time() - $mtime) > $ttl) {
94            @unlink($cacheFile);
95            return null;
96        }
97
98        $contents = @file_get_contents($cacheFile);
99        if ($contents === false) {
100            return null;
101        }
102
103        $data = @unserialize($contents);
104        if ($data === false) {
105            @unlink($cacheFile);
106            return null;
107        }
108
109        // Store in memory cache
110        self::$memoryCache[$key] = $data;
111
112        return $data;
113    }
114
115    /**
116     * Set cached events for a month
117     *
118     * @param string $namespace Namespace filter
119     * @param int $year Year
120     * @param int $month Month
121     * @param array $events Events data
122     * @return bool Success status
123     */
124    public static function setMonthEvents($namespace, $year, $month, array $events) {
125        $key = self::getMonthCacheKey($namespace, $year, $month);
126
127        // Store in memory cache
128        self::$memoryCache[$key] = $events;
129
130        $cacheFile = self::getCacheFile($key);
131        $serialized = serialize($events);
132
133        // Use temp file for atomic write
134        $tempFile = $cacheFile . '.tmp';
135        if (@file_put_contents($tempFile, $serialized) === false) {
136            return false;
137        }
138
139        if (!@rename($tempFile, $cacheFile)) {
140            @unlink($tempFile);
141            return false;
142        }
143
144        return true;
145    }
146
147    /**
148     * Invalidate cache for a specific month
149     *
150     * @param string $namespace Namespace filter
151     * @param int $year Year
152     * @param int $month Month
153     */
154    public static function invalidateMonth($namespace, $year, $month) {
155        $key = self::getMonthCacheKey($namespace, $year, $month);
156
157        // Clear memory cache
158        unset(self::$memoryCache[$key]);
159
160        // Delete cache file
161        $cacheFile = self::getCacheFile($key);
162        if (file_exists($cacheFile)) {
163            @unlink($cacheFile);
164        }
165    }
166
167    /**
168     * Invalidate cache for a namespace (all months)
169     *
170     * @param string $namespace Namespace to invalidate
171     */
172    public static function invalidateNamespace($namespace) {
173        $ns = preg_replace('/[^a-zA-Z0-9_-]/', '_', $namespace ?: 'default');
174        $pattern = self::getCacheDir() . "events_{$ns}_*.cache";
175
176        foreach (glob($pattern) as $file) {
177            @unlink($file);
178        }
179
180        // Clear matching memory cache entries
181        $prefix = "events_{$ns}_";
182        foreach (array_keys(self::$memoryCache) as $key) {
183            if (strpos($key, $prefix) === 0) {
184                unset(self::$memoryCache[$key]);
185            }
186        }
187    }
188
189    /**
190     * Invalidate all event caches
191     */
192    public static function invalidateAll() {
193        $pattern = self::getCacheDir() . "events_*.cache";
194
195        foreach (glob($pattern) as $file) {
196            @unlink($file);
197        }
198
199        // Clear memory cache
200        self::$memoryCache = [];
201    }
202
203    /**
204     * Get cache statistics
205     *
206     * @return array Cache stats
207     */
208    public static function getStats() {
209        $cacheDir = self::getCacheDir();
210        $files = glob($cacheDir . "*.cache");
211
212        $stats = [
213            'files' => count($files),
214            'size' => 0,
215            'oldest' => null,
216            'newest' => null,
217            'memory_entries' => count(self::$memoryCache)
218        ];
219
220        foreach ($files as $file) {
221            $size = filesize($file);
222            $mtime = filemtime($file);
223
224            $stats['size'] += $size;
225
226            if ($stats['oldest'] === null || $mtime < $stats['oldest']) {
227                $stats['oldest'] = $mtime;
228            }
229            if ($stats['newest'] === null || $mtime > $stats['newest']) {
230                $stats['newest'] = $mtime;
231            }
232        }
233
234        return $stats;
235    }
236
237    /**
238     * Clean up expired cache files
239     *
240     * @param int $ttl TTL in seconds
241     * @return int Number of files cleaned
242     */
243    public static function cleanup($ttl = self::DEFAULT_TTL) {
244        $cacheDir = self::getCacheDir();
245        $files = glob($cacheDir . "*.cache");
246        $cleaned = 0;
247        $now = time();
248
249        foreach ($files as $file) {
250            $mtime = filemtime($file);
251            if ($mtime !== false && ($now - $mtime) > $ttl) {
252                if (@unlink($file)) {
253                    $cleaned++;
254                }
255            }
256        }
257
258        return $cleaned;
259    }
260}
261