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