137748cd8SNickeau<?php 237748cd8SNickeau 3*c3437056SNickeauuse ComboStrap\AnalyticsDocument; 4*c3437056SNickeauuse ComboStrap\CacheExpirationDate; 537748cd8SNickeauuse ComboStrap\CacheManager; 61fa8c418SNickeauuse ComboStrap\CacheMedia; 7*c3437056SNickeauuse ComboStrap\Cron; 8*c3437056SNickeauuse ComboStrap\ExceptionCombo; 9*c3437056SNickeauuse ComboStrap\File; 101fa8c418SNickeauuse ComboStrap\Http; 1137748cd8SNickeauuse ComboStrap\Iso8601Date; 12*c3437056SNickeauuse ComboStrap\LogUtility; 13*c3437056SNickeauuse ComboStrap\MetadataDokuWikiStore; 14*c3437056SNickeauuse ComboStrap\Page; 15*c3437056SNickeauuse ComboStrap\PageDescription; 16*c3437056SNickeauuse ComboStrap\PageH1; 17*c3437056SNickeauuse ComboStrap\ResourceName; 18*c3437056SNickeauuse ComboStrap\PageTitle; 1937748cd8SNickeauuse ComboStrap\PluginUtility; 20*c3437056SNickeauuse ComboStrap\TplUtility; 211fa8c418SNickeauuse dokuwiki\Cache\CacheRenderer; 2237748cd8SNickeau 2337748cd8SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php'); 2437748cd8SNickeau 2537748cd8SNickeau/** 2637748cd8SNickeau * Can we use the parser cache 2737748cd8SNickeau */ 2837748cd8SNickeauclass action_plugin_combo_cache extends DokuWiki_Action_Plugin 2937748cd8SNickeau{ 3037748cd8SNickeau const COMBO_CACHE_PREFIX = "combo:cache:"; 3137748cd8SNickeau 321fa8c418SNickeau 331fa8c418SNickeau const CANONICAL = "cache"; 341fa8c418SNickeau const STATIC_SCRIPT_NAMES = ["/lib/exe/jquery.php", "/lib/exe/js.php", "/lib/exe/css.php"]; 351fa8c418SNickeau 361fa8c418SNickeau /** 37*c3437056SNickeau * @var string[] 38*c3437056SNickeau */ 39*c3437056SNickeau private static $sideSlotNames; 40*c3437056SNickeau 41*c3437056SNickeau 42*c3437056SNickeau private static function getSideSlotNames(): array 43*c3437056SNickeau { 44*c3437056SNickeau if (self::$sideSlotNames === null) { 45*c3437056SNickeau global $conf; 46*c3437056SNickeau 47*c3437056SNickeau self::$sideSlotNames = [ 48*c3437056SNickeau $conf['sidebar'] 49*c3437056SNickeau ]; 50*c3437056SNickeau 51*c3437056SNickeau /** 52*c3437056SNickeau * @see {@link \ComboStrap\TplConstant::CONF_SIDEKICK} 53*c3437056SNickeau */ 54*c3437056SNickeau $loaded = PluginUtility::loadStrapUtilityTemplateIfPresentAndSameVersion(); 55*c3437056SNickeau if ($loaded) { 56*c3437056SNickeau 57*c3437056SNickeau $sideKickSlotPageName = TplUtility::getSideKickSlotPageName(); 58*c3437056SNickeau if (!empty($sideKickSlotPageName)) { 59*c3437056SNickeau self::$sideSlotNames[] = $sideKickSlotPageName; 60*c3437056SNickeau } 61*c3437056SNickeau 62*c3437056SNickeau } 63*c3437056SNickeau } 64*c3437056SNickeau return self::$sideSlotNames; 65*c3437056SNickeau } 66*c3437056SNickeau 67*c3437056SNickeau private static function removeSideSlotCache() 68*c3437056SNickeau { 69*c3437056SNickeau $sidebars = self::getSideSlotNames(); 70*c3437056SNickeau 71*c3437056SNickeau 72*c3437056SNickeau /** 73*c3437056SNickeau * Delete the cache for the sidebar 74*c3437056SNickeau */ 75*c3437056SNickeau foreach ($sidebars as $sidebarRelativePath) { 76*c3437056SNickeau 77*c3437056SNickeau $page = Page::createPageFromNonQualifiedPath($sidebarRelativePath); 78*c3437056SNickeau $page->deleteCache(); 79*c3437056SNickeau 80*c3437056SNickeau } 81*c3437056SNickeau } 82*c3437056SNickeau 83*c3437056SNickeau /** 8437748cd8SNickeau * @param Doku_Event_Handler $controller 8537748cd8SNickeau */ 8637748cd8SNickeau function register(Doku_Event_Handler $controller) 8737748cd8SNickeau { 8837748cd8SNickeau 8937748cd8SNickeau /** 9037748cd8SNickeau * Log the cache usage and also 9137748cd8SNickeau */ 9237748cd8SNickeau $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'logCacheUsage', array()); 9337748cd8SNickeau 94*c3437056SNickeau $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'pageCacheExpiration', array()); 9537748cd8SNickeau 9637748cd8SNickeau /** 97*c3437056SNickeau * To add the cache result in the HTML 981fa8c418SNickeau */ 99*c3437056SNickeau $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addCacheLogHtmlDataBlock', array()); 10037748cd8SNickeau 10137748cd8SNickeau /** 10237748cd8SNickeau * To reset the cache manager 10337748cd8SNickeau * between two run in the test 10437748cd8SNickeau */ 10537748cd8SNickeau $controller->register_hook('DOKUWIKI_DONE', 'BEFORE', $this, 'close', array()); 10637748cd8SNickeau 1071fa8c418SNickeau /** 1081fa8c418SNickeau * To delete the VARY on css.php, jquery.php, js.php 1091fa8c418SNickeau */ 1101fa8c418SNickeau $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'deleteVaryFromStaticGeneratedResources', array()); 1111fa8c418SNickeau 112*c3437056SNickeau /** 113*c3437056SNickeau * To delete sidebar (cache) cache when a page was modified in a namespace 114*c3437056SNickeau * https://combostrap.com/sideslots 115*c3437056SNickeau */ 116*c3437056SNickeau $controller->register_hook(MetadataDokuWikiStore::PAGE_METADATA_MUTATION_EVENT, 'AFTER', $this, 'sideSlotsCacheBurstingForMetadataMutation', array()); 117*c3437056SNickeau $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'sideSlotsCacheBurstingForPageCreationAndDeletion', array()); 1181fa8c418SNickeau 11937748cd8SNickeau } 12037748cd8SNickeau 12137748cd8SNickeau /** 12237748cd8SNickeau * 12337748cd8SNickeau * @param Doku_Event $event 12437748cd8SNickeau * @param $params 12537748cd8SNickeau */ 12637748cd8SNickeau function logCacheUsage(Doku_Event $event, $params) 12737748cd8SNickeau { 12837748cd8SNickeau 12937748cd8SNickeau /** 13037748cd8SNickeau * To log the cache used by bar 13137748cd8SNickeau * @var \dokuwiki\Cache\CacheParser $data 13237748cd8SNickeau */ 13337748cd8SNickeau $data = $event->data; 13437748cd8SNickeau $result = $event->result; 135*c3437056SNickeau $slotId = $data->page; 13637748cd8SNickeau $cacheManager = PluginUtility::getCacheManager(); 137*c3437056SNickeau $cacheManager->addSlotForRequestedPage($slotId, $result, $data); 13837748cd8SNickeau 13937748cd8SNickeau 14037748cd8SNickeau } 14137748cd8SNickeau 14237748cd8SNickeau /** 14337748cd8SNickeau * 144*c3437056SNickeau * Purge the cache if needed 14537748cd8SNickeau * @param Doku_Event $event 14637748cd8SNickeau * @param $params 14737748cd8SNickeau */ 148*c3437056SNickeau function pageCacheExpiration(Doku_Event $event, $params) 14937748cd8SNickeau { 15037748cd8SNickeau 15137748cd8SNickeau /** 15237748cd8SNickeau * No cache for all mode 15337748cd8SNickeau * (ie xhtml, instruction) 15437748cd8SNickeau */ 15537748cd8SNickeau $data = &$event->data; 15637748cd8SNickeau $pageId = $data->page; 1571fa8c418SNickeau 1581fa8c418SNickeau /** 159*c3437056SNickeau * For whatever reason, the cache file of XHTML 1601fa8c418SNickeau * may be empty - No error found on the web server or the log. 1611fa8c418SNickeau * 1621fa8c418SNickeau * We just delete it then. 1631fa8c418SNickeau * 1641fa8c418SNickeau * It has been seen after the creation of a new page or a `move` of the page. 1651fa8c418SNickeau */ 1661fa8c418SNickeau if ($data instanceof CacheRenderer) { 1671fa8c418SNickeau if ($data->mode === "xhtml") { 1681fa8c418SNickeau if (file_exists($data->cache)) { 1691fa8c418SNickeau if (filesize($data->cache) === 0) { 1701fa8c418SNickeau $data->depends["purge"] = true; 1711fa8c418SNickeau } 1721fa8c418SNickeau } 1731fa8c418SNickeau } 1741fa8c418SNickeau } 17537748cd8SNickeau /** 17637748cd8SNickeau * Because of the recursive nature of rendering 17737748cd8SNickeau * inside dokuwiki, we just handle the first 17837748cd8SNickeau * rendering for a request. 17937748cd8SNickeau * 18037748cd8SNickeau * The first will be purged, the other one not 181*c3437056SNickeau * because they can't use the first one 18237748cd8SNickeau */ 183*c3437056SNickeau if (!PluginUtility::getCacheManager()->isCacheLogPresentForSlot($pageId, $data->mode)) { 184*c3437056SNickeau $page = Page::createPageFromId($pageId); 185*c3437056SNickeau $cacheExpirationFrequency = $page->getCacheExpirationFrequency(); 186*c3437056SNickeau if ($cacheExpirationFrequency === null) { 187*c3437056SNickeau return; 188*c3437056SNickeau } 18937748cd8SNickeau 190*c3437056SNickeau $expirationDate = CacheExpirationDate::createForPage($page) 191*c3437056SNickeau ->getValue(); 192*c3437056SNickeau 193*c3437056SNickeau if ($expirationDate === null) { 194*c3437056SNickeau try { 195*c3437056SNickeau $expirationDate = Cron::getDate($cacheExpirationFrequency); 196*c3437056SNickeau $page->setCacheExpirationDate($expirationDate); 197*c3437056SNickeau } catch (ExceptionCombo $e) { 198*c3437056SNickeau LogUtility::msg("The cache expiration frequency ($cacheExpirationFrequency) is not a valid cron expression"); 199*c3437056SNickeau } 200*c3437056SNickeau } 201*c3437056SNickeau if ($expirationDate !== null) { 202*c3437056SNickeau 20337748cd8SNickeau $actualDate = new DateTime(); 20437748cd8SNickeau if ($expirationDate < $actualDate) { 20537748cd8SNickeau /** 20637748cd8SNickeau * As seen in {@link Cache::makeDefaultCacheDecision()} 20737748cd8SNickeau * We request a purge 20837748cd8SNickeau */ 20937748cd8SNickeau $data->depends["purge"] = true; 210*c3437056SNickeau 211*c3437056SNickeau /** 212*c3437056SNickeau * Calculate a new expiration date 213*c3437056SNickeau */ 214*c3437056SNickeau try { 215*c3437056SNickeau $newDate = Cron::getDate($cacheExpirationFrequency); 216*c3437056SNickeau if ($newDate < $actualDate) { 217*c3437056SNickeau LogUtility::msg("The new calculated date cache expiration frequency ({$newDate->format(Iso8601Date::getFormat())}) is lower than the current date ({$actualDate->format(Iso8601Date::getFormat())})"); 218*c3437056SNickeau } 219*c3437056SNickeau $page->setCacheExpirationDate($newDate); 220*c3437056SNickeau } catch (ExceptionCombo $e) { 221*c3437056SNickeau LogUtility::msg("The cache expiration frequency ($cacheExpirationFrequency) is not a value cron expression"); 222*c3437056SNickeau } 22337748cd8SNickeau } 22437748cd8SNickeau } 22537748cd8SNickeau } 22637748cd8SNickeau 22737748cd8SNickeau 22837748cd8SNickeau } 22937748cd8SNickeau 23037748cd8SNickeau /** 23137748cd8SNickeau * Add HTML meta to be able to debug 23237748cd8SNickeau * @param Doku_Event $event 23337748cd8SNickeau * @param $params 23437748cd8SNickeau */ 235*c3437056SNickeau function addCacheLogHtmlDataBlock(Doku_Event $event, $params) 23637748cd8SNickeau { 23737748cd8SNickeau 23837748cd8SNickeau $cacheManager = PluginUtility::getCacheManager(); 239*c3437056SNickeau $cacheSlotResults = $cacheManager->getCacheSlotResultsAsHtmlDataBlockArray(); 240*c3437056SNickeau $cacheJson = \ComboStrap\Json::createFromArray($cacheSlotResults); 24137748cd8SNickeau 242*c3437056SNickeau if (PluginUtility::isDevOrTest()) { 243*c3437056SNickeau $result = $cacheJson->toPrettyJsonString(); 24437748cd8SNickeau } else { 245*c3437056SNickeau $result = $cacheJson->toMinifiedJsonString(); 24637748cd8SNickeau } 24737748cd8SNickeau 248*c3437056SNickeau $event->data["script"][] = array( 249*c3437056SNickeau "type" => CacheManager::APPLICATION_COMBO_CACHE_JSON, 250*c3437056SNickeau "_data" => $result, 251*c3437056SNickeau ); 25237748cd8SNickeau 25337748cd8SNickeau } 25437748cd8SNickeau 25537748cd8SNickeau function close(Doku_Event $event, $params) 25637748cd8SNickeau { 257*c3437056SNickeau CacheManager::reset(); 2581fa8c418SNickeau } 2591fa8c418SNickeau 2601fa8c418SNickeau 2611fa8c418SNickeau /** 2621fa8c418SNickeau * Delete the Vary header 2631fa8c418SNickeau * @param Doku_Event $event 2641fa8c418SNickeau * @param $params 2651fa8c418SNickeau */ 2661fa8c418SNickeau public static function deleteVaryFromStaticGeneratedResources(Doku_Event $event, $params) 2671fa8c418SNickeau { 2681fa8c418SNickeau 2691fa8c418SNickeau $script = $_SERVER["SCRIPT_NAME"]; 2701fa8c418SNickeau if (in_array($script, self::STATIC_SCRIPT_NAMES)) { 271*c3437056SNickeau // To be extra sure, they must have the buster key 272*c3437056SNickeau if (isset($_REQUEST[CacheMedia::CACHE_BUSTER_KEY])) { 2731fa8c418SNickeau self::deleteVaryHeader(); 2741fa8c418SNickeau } 2751fa8c418SNickeau } 2761fa8c418SNickeau 2771fa8c418SNickeau } 2781fa8c418SNickeau 2791fa8c418SNickeau /** 2801fa8c418SNickeau * 2811fa8c418SNickeau * No Vary: Cookie 2821fa8c418SNickeau * Introduced at 2831fa8c418SNickeau * https://github.com/splitbrain/dokuwiki/issues/1594 2841fa8c418SNickeau * But cache problem at: 2851fa8c418SNickeau * https://github.com/splitbrain/dokuwiki/issues/2520 2861fa8c418SNickeau * 2871fa8c418SNickeau */ 2881fa8c418SNickeau public static function deleteVaryHeader(): void 2891fa8c418SNickeau { 290*c3437056SNickeau if (PluginUtility::getConfValue(action_plugin_combo_staticresource::CONF_STATIC_CACHE_ENABLED, 1)) { 2911fa8c418SNickeau Http::removeHeaderIfPresent("Vary"); 2921fa8c418SNickeau } 2931fa8c418SNickeau } 2941fa8c418SNickeau 295*c3437056SNickeau function sideSlotsCacheBurstingForMetadataMutation($event) 296*c3437056SNickeau { 297*c3437056SNickeau 298*c3437056SNickeau $data = $event->data; 299*c3437056SNickeau /** 300*c3437056SNickeau * The side slot cache is deleted only when the 301*c3437056SNickeau * below property are updated 302*c3437056SNickeau */ 303*c3437056SNickeau $descriptionProperties = [PageTitle::PROPERTY_NAME, ResourceName::PROPERTY_NAME, PageH1::PROPERTY_NAME, PageDescription::DESCRIPTION_PROPERTY]; 304*c3437056SNickeau if (!in_array($data["name"], $descriptionProperties)) return; 305*c3437056SNickeau 306*c3437056SNickeau self::removeSideSlotCache(); 307*c3437056SNickeau 308*c3437056SNickeau } 309*c3437056SNickeau 310*c3437056SNickeau /** 311*c3437056SNickeau * @param $event 312*c3437056SNickeau * @throws Exception 313*c3437056SNickeau * @link https://www.dokuwiki.org/devel:event:io_wikipage_write 314*c3437056SNickeau */ 315*c3437056SNickeau function sideSlotsCacheBurstingForPageCreationAndDeletion($event) 316*c3437056SNickeau { 317*c3437056SNickeau 318*c3437056SNickeau $data = $event->data; 319*c3437056SNickeau $pageName = $data[2]; 320*c3437056SNickeau 321*c3437056SNickeau /** 322*c3437056SNickeau * Modification to the side slot is not processed further 323*c3437056SNickeau */ 324*c3437056SNickeau if (in_array($pageName, self::getSideSlotNames())) return; 325*c3437056SNickeau 326*c3437056SNickeau /** 327*c3437056SNickeau * Pointer to see if we need to delete the cache 328*c3437056SNickeau */ 329*c3437056SNickeau $doWeNeedToDeleteTheSideSlotCache = false; 330*c3437056SNickeau 331*c3437056SNickeau /** 332*c3437056SNickeau * File creation 333*c3437056SNickeau * 334*c3437056SNickeau * ``` 335*c3437056SNickeau * Page creation may be detected by checking if the file already exists and the revision is false. 336*c3437056SNickeau * ``` 337*c3437056SNickeau * From https://www.dokuwiki.org/devel:event:io_wikipage_write 338*c3437056SNickeau * 339*c3437056SNickeau */ 340*c3437056SNickeau $rev = $data[3]; 341*c3437056SNickeau $filePath = $data[0][0]; 342*c3437056SNickeau $file = File::createFromPath($filePath); 343*c3437056SNickeau if (!$file->exists() && $rev === false) { 344*c3437056SNickeau $doWeNeedToDeleteTheSideSlotCache = true; 345*c3437056SNickeau } 346*c3437056SNickeau 347*c3437056SNickeau /** 348*c3437056SNickeau * File deletion 349*c3437056SNickeau * (No content) 350*c3437056SNickeau * 351*c3437056SNickeau * ``` 352*c3437056SNickeau * Page deletion may be detected by checking for empty page content. 353*c3437056SNickeau * On update to an existing page this event is called twice, once for the transfer of the old version to the attic (rev will have a value) 354*c3437056SNickeau * and once to write the new version of the page into the wiki (rev is false) 355*c3437056SNickeau * ``` 356*c3437056SNickeau * From https://www.dokuwiki.org/devel:event:io_wikipage_write 357*c3437056SNickeau */ 358*c3437056SNickeau $append = $data[0][2]; 359*c3437056SNickeau if (!$append) { 360*c3437056SNickeau 361*c3437056SNickeau $content = $data[0][1]; 362*c3437056SNickeau if (empty($content) && $rev === false) { 363*c3437056SNickeau // Deletion 364*c3437056SNickeau $doWeNeedToDeleteTheSideSlotCache = true; 365*c3437056SNickeau } 366*c3437056SNickeau 367*c3437056SNickeau } 368*c3437056SNickeau 369*c3437056SNickeau if ($doWeNeedToDeleteTheSideSlotCache) self::removeSideSlotCache(); 370*c3437056SNickeau 371*c3437056SNickeau } 37237748cd8SNickeau 37337748cd8SNickeau} 374