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