1<?php 2 3 4namespace ComboStrap; 5 6use dokuwiki\Cache\CacheParser; 7use dokuwiki\Cache\CacheRenderer; 8use splitbrain\slika\Exception; 9 10/** 11 * 12 * @package ComboStrap 13 * 14 * Manage the cache dependencies for a slot level (not instructions cache). 15 * 16 * The dependencies are stored on a file system. 17 * 18 * Cache dependencies are used: 19 * * to generate the cache key output 20 * * to add cache validity dependency such as requested page, 21 * 22 * For cache key generation, this is mostly used on 23 * * side slots to have several output of a list {@link \syntax_plugin_combo_pageexplorer navigation pane} for different namespace (ie there is one cache by namespace) 24 * * header and footer main slot to have one output for each requested main page 25 */ 26class MarkupCacheDependencies 27{ 28 /** 29 * The dependency value is the requested page path 30 * (used for syntax mostly used in the header and footer of the main slot for instance) 31 */ 32 public const REQUESTED_PAGE_DEPENDENCY = "requested_page"; 33 /** 34 * The special scope value current means the namespace of the requested page 35 * The real scope value is then calculated before retrieving the cache 36 */ 37 public const REQUESTED_NAMESPACE_DEPENDENCY = "requested_namespace"; 38 /** 39 * @deprecated use the {@link MarkupCacheDependencies::REQUESTED_NAMESPACE_DEPENDENCY} 40 */ 41 public const NAMESPACE_OLD_VALUE = "current"; 42 43 /** 44 * This dependencies have an impact on the 45 * output location of the cache 46 * {@link MarkupCacheDependencies::getOrCalculateDependencyKey()} 47 */ 48 public const OUTPUT_DEPENDENCIES = [self::REQUESTED_PAGE_DEPENDENCY, self::REQUESTED_NAMESPACE_DEPENDENCY]; 49 50 /** 51 * This dependencies have an impact on the freshness 52 * of the cache 53 */ 54 public const validityDependencies = [ 55 self::BACKLINKS_DEPENDENCY, 56 self::SQL_DEPENDENCY, 57 self::PAGE_PRIMARY_META_DEPENDENCY, 58 self::PAGE_SYSTEM_DEPENDENCY 59 ]; 60 61 /** 62 * Backlinks are printed in the page 63 * {@link \action_plugin_combo_backlinkmutation} 64 * If a referent page add or delete a link, 65 * the slot should be refreshed / cache should be deleted 66 */ 67 const BACKLINKS_DEPENDENCY = "backlinks"; 68 69 /** 70 * A page sql is in the page 71 * (The page should be refreshed by default once a day) 72 */ 73 const SQL_DEPENDENCY = "sql"; 74 75 /** 76 * If the name, the title, the h1 or the description 77 * of a page changes, the cache should be invalidated 78 * See {@link \action_plugin_combo_pageprimarymetamutation} 79 */ 80 const PAGE_PRIMARY_META_DEPENDENCY = "page_primary_meta"; 81 82 /** 83 * If a page is added or deleted 84 * See {@link \action_plugin_combo_pagesystemmutation} 85 */ 86 const PAGE_SYSTEM_DEPENDENCY = "page_system"; 87 88 const CANONICAL = "cache:dependency"; 89 90 91 /** 92 * @var CacheParser 93 */ 94 private $dependenciesCacheStore; 95 96 97 /** 98 * @var array list of dependencies to calculate the cache key 99 * 100 * In a general pattern, a dependency is a series of function that would output runtime data 101 * that should go into the render cache key such as user logged in, requested page, namespace of the requested page, ... 102 * 103 * The cache dependencies data are saved alongside the page (same as snippets) 104 * 105 */ 106 private $runtimeAddedDependencies = null; 107 /** 108 * The stored runtime dependencies 109 * @var array 110 */ 111 private $runtimeStoreDependencies; 112 113 114 /** 115 * @var string the first key captured 116 */ 117 private $firstActualKey; 118 private FetcherMarkup $markupFetcher; 119 120 121 /** 122 * CacheManagerForSlot constructor. 123 * 124 */ 125 private function __construct(FetcherMarkup $markupFetcher) 126 { 127 128 $this->markupFetcher = $markupFetcher; 129 $executingPath = $markupFetcher->getExecutingPathOrNull(); 130 if ($executingPath !== null) { 131 $data = $this->getDependenciesCacheStore()->retrieveCache(); 132 if (!empty($data)) { 133 $this->runtimeStoreDependencies = json_decode($data, true); 134 } 135 } 136 137 } 138 139 public static function create(FetcherMarkup $fetcherMarkup): MarkupCacheDependencies 140 { 141 return new MarkupCacheDependencies($fetcherMarkup); 142 } 143 144 /** 145 * Rerender for now only the secondary slot if it has cache dependency 146 * (ie {@link MarkupCacheDependencies::PAGE_SYSTEM_DEPENDENCY} or {@link MarkupCacheDependencies::PAGE_PRIMARY_META_DEPENDENCY}) 147 * @param $pathAddedOrDeleted 148 * @param string $dependency - a {@link MarkupCacheDependencies} ie 149 * @param string $event 150 */ 151 public static function reRenderSideSlotIfNeeded($pathAddedOrDeleted, string $dependency, string $event) 152 { 153 154 /** 155 * Rerender secondary slot if needed 156 */ 157 $page = MarkupPath::createMarkupFromStringPath($pathAddedOrDeleted); 158 $pageWikiPath = $page->getPathObject(); 159 if (!($pageWikiPath instanceof WikiPath)) { 160 LogUtility::errorIfDevOrTest("The path should be a wiki path"); 161 return; 162 } 163 $slots = [$page->getSideSlot()]; 164 foreach ($slots as $slot) { 165 if ($slot === null) { 166 continue; 167 } 168 try { 169 $slotFetcher = FetcherMarkup::confRoot() 170 ->setRequestedMimeToXhtml() 171 ->setRequestedContextPath($pageWikiPath) 172 ->setRequestedExecutingPath($slot) 173 ->build(); 174 } catch (ExceptionNotExists $e) { 175 // layout fragment does not exists 176 continue; 177 } 178 $cacheDependencies = $slotFetcher->getOutputCacheDependencies(); 179 if ($cacheDependencies->hasDependency($dependency)) { 180 $link = PluginUtility::getDocumentationHyperLink("cache:slot", "Slot Dependency", false); 181 $message = "$link ($dependency) was met with the primary slot ($pathAddedOrDeleted)."; 182 CacheLog::deleteCacheIfExistsAndLog( 183 $slotFetcher, 184 $event, 185 $message 186 ); 187 CacheLog::renderCacheAndLog( 188 $slotFetcher, 189 $event, 190 $message 191 ); 192 } 193 194 } 195 196 } 197 198 199 /** 200 * @return string - output the namespace used in the cache key 201 * 202 * For example: 203 * * the ':sidebar' html output may be dependent to the namespace `ns` or `ns2` 204 */ 205 public function getValueForKey($dependenciesValue): string 206 { 207 208 /** 209 * Set the logical id 210 * When no $ID is set (for instance, test), 211 * the logical id is the id 212 * 213 * The logical id depends on the namespace attribute of the {@link \syntax_plugin_combo_pageexplorer} 214 * stored in the `scope` metadata. 215 * 216 * Scope is directory/namespace based 217 */ 218 $path = $this->markupFetcher->getRequestedContextPath(); 219 $requestedPage = MarkupPath::createPageFromPathObject($path); 220 switch ($dependenciesValue) { 221 case MarkupCacheDependencies::NAMESPACE_OLD_VALUE: 222 case MarkupCacheDependencies::REQUESTED_NAMESPACE_DEPENDENCY: 223 try { 224 $parentPath = $requestedPage->getPathObject()->getParent(); 225 return $parentPath->toAbsoluteId(); 226 } catch (ExceptionNotFound $e) { 227 // root 228 return ":"; 229 } 230 case MarkupCacheDependencies::REQUESTED_PAGE_DEPENDENCY: 231 return $requestedPage->getPathObject()->toAbsoluteId(); 232 default: 233 throw new ExceptionRuntimeInternal("The requested dependency value ($dependenciesValue) has no calculation"); 234 } 235 236 237 } 238 239 /** 240 * @return string 241 * 242 * Cache is now managed by dependencies function that creates a unique key 243 * for the instruction document and the output document 244 * 245 * See the discussion at: https://github.com/splitbrain/dokuwiki/issues/3496 246 * @throws ExceptionCompile 247 * @var $actualKey 248 */ 249 public function getOrCalculateDependencyKey($actualKey): string 250 { 251 /** 252 * We should wrap a call only once 253 * We capture therefore the first actual key passed 254 */ 255 if ($this->firstActualKey === null) { 256 $this->firstActualKey = $actualKey; 257 } 258 $dependencyKey = $this->firstActualKey; 259 $runtimeDependencies = $this->getDependencies(); 260 261 foreach ($runtimeDependencies as $dependency) { 262 if (in_array($dependency, self::OUTPUT_DEPENDENCIES)) { 263 $dependencyKey .= $this->getValueForKey($dependency); 264 } 265 } 266 return $dependencyKey; 267 } 268 269 270 /** 271 * @param string $dependencyName 272 * @return MarkupCacheDependencies 273 */ 274 public function addDependency(string $dependencyName): MarkupCacheDependencies 275 { 276 if (PluginUtility::isDevOrTest()) { 277 if (!in_array($dependencyName, self::OUTPUT_DEPENDENCIES) && 278 !in_array($dependencyName, self::validityDependencies) 279 ) { 280 throw new ExceptionRuntime("Unknown dependency value ($dependencyName)"); 281 } 282 } 283 $this->runtimeAddedDependencies[$dependencyName] = ""; 284 return $this; 285 } 286 287 public 288 function getDependencies(): array 289 { 290 if ($this->runtimeAddedDependencies !== null) { 291 return array_keys($this->runtimeAddedDependencies); 292 } 293 if ($this->runtimeStoreDependencies === null) { 294 return []; 295 } 296 return array_keys($this->runtimeStoreDependencies); 297 } 298 299 /** 300 * The default key as seen in {@link CacheParser} 301 * Used for test purpose 302 * @return string 303 */ 304 public 305 function getDefaultKey(): string 306 { 307 try { 308 try { 309 // dokuwiki cache key compatible 310 $wikiId = $this->markupFetcher->getRequestedExecutingPath()->toWikiPath()->getWikiId(); 311 $absoluteString = wikiFN($wikiId); 312 } catch (ExceptionCast|ExceptionNotFound $e) { 313 $absoluteString = $this->markupFetcher->getRequestedExecutingPath()->toAbsoluteId(); 314 } 315 $keyDokuWikiCompliant = str_replace("\\", "/", $absoluteString); 316 return $keyDokuWikiCompliant . $_SERVER['HTTP_HOST'] . $_SERVER['SERVER_PORT']; 317 } catch (ExceptionNotFound $e) { 318 throw new ExceptionRuntimeInternal("No executing path to calculate the cache key"); 319 } 320 321 } 322 323 /** 324 * Snippet.json, Cache dependency are data dependent 325 * 326 * For instance, the carrousel may add glide or grid as snippet. It depends on the the number of backlinks. 327 * 328 * Therefore the output should be unique by rendered fragment 329 * Therefore we reroute (recalculate the cache key to be the same than the html file) 330 * 331 * @param CacheParser $cache 332 * @return void 333 */ 334 public 335 function rerouteCacheDestination(CacheParser &$cache) 336 { 337 338 try { 339 340 $cache->key = $this->getOrCalculateDependencyKey($cache->key); 341 $cache->cache = getCacheName($cache->key, '.' . $cache->mode); 342 343 } catch (ExceptionCompile $e) { 344 LogUtility::msg("Error while trying to reroute the content cache destination for the fetcher ({$this->markupFetcher}). You may have cache problem. Error: {$e->getMessage()}"); 345 } 346 347 } 348 349 350 /** 351 */ 352 public function storeDependencies() 353 { 354 355 /** 356 * Cache file 357 * Using a cache parser, set the page id and will trigger 358 * the parser cache use event in order to log/report the cache usage 359 * At {@link action_plugin_combo_cache::createCacheReport()} 360 */ 361 $dependencies = $this->getDependenciesCacheStore(); 362 $deps = $this->runtimeAddedDependencies; 363 if ($deps !== null) { 364 $jsonDeps = json_encode($deps); 365 $dependencies->storeCache($jsonDeps); 366 } else { 367 // dependencies does not exist or were removed 368 $dependencies->removeCache(); 369 } 370 371 372 } 373 374 public 375 function getDependenciesCacheStore(): CacheParser 376 { 377 if ($this->dependenciesCacheStore !== null) { 378 return $this->dependenciesCacheStore; 379 } 380 381 /** 382 * The local path to calculate the full qualified Os path 383 */ 384 try { 385 $executingPath = $this->markupFetcher->getRequestedExecutingPath(); 386 } catch (ExceptionNotFound $e) { 387 throw new ExceptionRuntimeInternal("There is no executing path, you can create a cache dependencies store", self::CANONICAL); 388 } 389 390 list($wikiId, $localPath) = FetcherMarkupBuilder::getWikiIdAndLocalFileDokuwikiCompliant($executingPath); 391 $this->dependenciesCacheStore = new CacheParser($wikiId, $localPath, "deps.json"); 392 return $this->dependenciesCacheStore; 393 } 394 395 public 396 function hasDependency(string $dependencyName): bool 397 { 398 $dependencies = $this->getDependencies(); 399 return in_array($dependencyName, $dependencies); 400 } 401 402} 403