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