1<?php 2 3use ComboStrap\AnalyticsDocument; 4use ComboStrap\CacheExpirationDate; 5use ComboStrap\CacheManager; 6use ComboStrap\CacheMedia; 7use ComboStrap\Cron; 8use ComboStrap\ExceptionCombo; 9use ComboStrap\File; 10use ComboStrap\Http; 11use ComboStrap\Iso8601Date; 12use ComboStrap\LogUtility; 13use ComboStrap\MetadataDokuWikiStore; 14use ComboStrap\Page; 15use ComboStrap\PageDescription; 16use ComboStrap\PageH1; 17use ComboStrap\ResourceName; 18use ComboStrap\PageTitle; 19use ComboStrap\PluginUtility; 20use ComboStrap\TplUtility; 21use dokuwiki\Cache\CacheRenderer; 22 23require_once(__DIR__ . '/../ComboStrap/PluginUtility.php'); 24 25/** 26 * Can we use the parser cache 27 */ 28class action_plugin_combo_cache extends DokuWiki_Action_Plugin 29{ 30 const COMBO_CACHE_PREFIX = "combo:cache:"; 31 32 33 const CANONICAL = "cache"; 34 const STATIC_SCRIPT_NAMES = ["/lib/exe/jquery.php", "/lib/exe/js.php", "/lib/exe/css.php"]; 35 36 /** 37 * @var string[] 38 */ 39 private static $sideSlotNames; 40 41 42 private static function getSideSlotNames(): array 43 { 44 if (self::$sideSlotNames === null) { 45 global $conf; 46 47 self::$sideSlotNames = [ 48 $conf['sidebar'] 49 ]; 50 51 /** 52 * @see {@link \ComboStrap\TplConstant::CONF_SIDEKICK} 53 */ 54 $loaded = PluginUtility::loadStrapUtilityTemplateIfPresentAndSameVersion(); 55 if ($loaded) { 56 57 $sideKickSlotPageName = TplUtility::getSideKickSlotPageName(); 58 if (!empty($sideKickSlotPageName)) { 59 self::$sideSlotNames[] = $sideKickSlotPageName; 60 } 61 62 } 63 } 64 return self::$sideSlotNames; 65 } 66 67 private static function removeSideSlotCache() 68 { 69 $sidebars = self::getSideSlotNames(); 70 71 72 /** 73 * Delete the cache for the sidebar 74 */ 75 foreach ($sidebars as $sidebarRelativePath) { 76 77 $page = Page::createPageFromNonQualifiedPath($sidebarRelativePath); 78 $page->deleteCache(); 79 80 } 81 } 82 83 /** 84 * @param Doku_Event_Handler $controller 85 */ 86 function register(Doku_Event_Handler $controller) 87 { 88 89 /** 90 * Log the cache usage and also 91 */ 92 $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'logCacheUsage', array()); 93 94 $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'pageCacheExpiration', array()); 95 96 /** 97 * To add the cache result in the HTML 98 */ 99 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addCacheLogHtmlDataBlock', array()); 100 101 /** 102 * To reset the cache manager 103 * between two run in the test 104 */ 105 $controller->register_hook('DOKUWIKI_DONE', 'BEFORE', $this, 'close', array()); 106 107 /** 108 * To delete the VARY on css.php, jquery.php, js.php 109 */ 110 $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'deleteVaryFromStaticGeneratedResources', array()); 111 112 /** 113 * To delete sidebar (cache) cache when a page was modified in a namespace 114 * https://combostrap.com/sideslots 115 */ 116 $controller->register_hook(MetadataDokuWikiStore::PAGE_METADATA_MUTATION_EVENT, 'AFTER', $this, 'sideSlotsCacheBurstingForMetadataMutation', array()); 117 $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'sideSlotsCacheBurstingForPageCreationAndDeletion', array()); 118 119 } 120 121 /** 122 * 123 * @param Doku_Event $event 124 * @param $params 125 */ 126 function logCacheUsage(Doku_Event $event, $params) 127 { 128 129 /** 130 * To log the cache used by bar 131 * @var \dokuwiki\Cache\CacheParser $data 132 */ 133 $data = $event->data; 134 $result = $event->result; 135 $slotId = $data->page; 136 $cacheManager = PluginUtility::getCacheManager(); 137 $cacheManager->addSlotForRequestedPage($slotId, $result, $data); 138 139 140 } 141 142 /** 143 * 144 * Purge the cache if needed 145 * @param Doku_Event $event 146 * @param $params 147 */ 148 function pageCacheExpiration(Doku_Event $event, $params) 149 { 150 151 /** 152 * No cache for all mode 153 * (ie xhtml, instruction) 154 */ 155 $data = &$event->data; 156 $pageId = $data->page; 157 158 /** 159 * For whatever reason, the cache file of XHTML 160 * may be empty - No error found on the web server or the log. 161 * 162 * We just delete it then. 163 * 164 * It has been seen after the creation of a new page or a `move` of the page. 165 */ 166 if ($data instanceof CacheRenderer) { 167 if ($data->mode === "xhtml") { 168 if (file_exists($data->cache)) { 169 if (filesize($data->cache) === 0) { 170 $data->depends["purge"] = true; 171 } 172 } 173 } 174 } 175 /** 176 * Because of the recursive nature of rendering 177 * inside dokuwiki, we just handle the first 178 * rendering for a request. 179 * 180 * The first will be purged, the other one not 181 * because they can't use the first one 182 */ 183 if (!PluginUtility::getCacheManager()->isCacheLogPresentForSlot($pageId, $data->mode)) { 184 $page = Page::createPageFromId($pageId); 185 $cacheExpirationFrequency = $page->getCacheExpirationFrequency(); 186 if ($cacheExpirationFrequency === null) { 187 return; 188 } 189 190 $expirationDate = CacheExpirationDate::createForPage($page) 191 ->getValue(); 192 193 if ($expirationDate === null) { 194 try { 195 $expirationDate = Cron::getDate($cacheExpirationFrequency); 196 $page->setCacheExpirationDate($expirationDate); 197 } catch (ExceptionCombo $e) { 198 LogUtility::msg("The cache expiration frequency ($cacheExpirationFrequency) is not a valid cron expression"); 199 } 200 } 201 if ($expirationDate !== null) { 202 203 $actualDate = new DateTime(); 204 if ($expirationDate < $actualDate) { 205 /** 206 * As seen in {@link Cache::makeDefaultCacheDecision()} 207 * We request a purge 208 */ 209 $data->depends["purge"] = true; 210 211 /** 212 * Calculate a new expiration date 213 */ 214 try { 215 $newDate = Cron::getDate($cacheExpirationFrequency); 216 if ($newDate < $actualDate) { 217 LogUtility::msg("The new calculated date cache expiration frequency ({$newDate->format(Iso8601Date::getFormat())}) is lower than the current date ({$actualDate->format(Iso8601Date::getFormat())})"); 218 } 219 $page->setCacheExpirationDate($newDate); 220 } catch (ExceptionCombo $e) { 221 LogUtility::msg("The cache expiration frequency ($cacheExpirationFrequency) is not a value cron expression"); 222 } 223 } 224 } 225 } 226 227 228 } 229 230 /** 231 * Add HTML meta to be able to debug 232 * @param Doku_Event $event 233 * @param $params 234 */ 235 function addCacheLogHtmlDataBlock(Doku_Event $event, $params) 236 { 237 238 $cacheManager = PluginUtility::getCacheManager(); 239 $cacheSlotResults = $cacheManager->getCacheSlotResultsAsHtmlDataBlockArray(); 240 $cacheJson = \ComboStrap\Json::createFromArray($cacheSlotResults); 241 242 if (PluginUtility::isDevOrTest()) { 243 $result = $cacheJson->toPrettyJsonString(); 244 } else { 245 $result = $cacheJson->toMinifiedJsonString(); 246 } 247 248 $event->data["script"][] = array( 249 "type" => CacheManager::APPLICATION_COMBO_CACHE_JSON, 250 "_data" => $result, 251 ); 252 253 } 254 255 function close(Doku_Event $event, $params) 256 { 257 CacheManager::reset(); 258 } 259 260 261 /** 262 * Delete the Vary header 263 * @param Doku_Event $event 264 * @param $params 265 */ 266 public static function deleteVaryFromStaticGeneratedResources(Doku_Event $event, $params) 267 { 268 269 $script = $_SERVER["SCRIPT_NAME"]; 270 if (in_array($script, self::STATIC_SCRIPT_NAMES)) { 271 // To be extra sure, they must have the buster key 272 if (isset($_REQUEST[CacheMedia::CACHE_BUSTER_KEY])) { 273 self::deleteVaryHeader(); 274 } 275 } 276 277 } 278 279 /** 280 * 281 * No Vary: Cookie 282 * Introduced at 283 * https://github.com/splitbrain/dokuwiki/issues/1594 284 * But cache problem at: 285 * https://github.com/splitbrain/dokuwiki/issues/2520 286 * 287 */ 288 public static function deleteVaryHeader(): void 289 { 290 if (PluginUtility::getConfValue(action_plugin_combo_staticresource::CONF_STATIC_CACHE_ENABLED, 1)) { 291 Http::removeHeaderIfPresent("Vary"); 292 } 293 } 294 295 function sideSlotsCacheBurstingForMetadataMutation($event) 296 { 297 298 $data = $event->data; 299 /** 300 * The side slot cache is deleted only when the 301 * below property are updated 302 */ 303 $descriptionProperties = [PageTitle::PROPERTY_NAME, ResourceName::PROPERTY_NAME, PageH1::PROPERTY_NAME, PageDescription::DESCRIPTION_PROPERTY]; 304 if (!in_array($data["name"], $descriptionProperties)) return; 305 306 self::removeSideSlotCache(); 307 308 } 309 310 /** 311 * @param $event 312 * @throws Exception 313 * @link https://www.dokuwiki.org/devel:event:io_wikipage_write 314 */ 315 function sideSlotsCacheBurstingForPageCreationAndDeletion($event) 316 { 317 318 $data = $event->data; 319 $pageName = $data[2]; 320 321 /** 322 * Modification to the side slot is not processed further 323 */ 324 if (in_array($pageName, self::getSideSlotNames())) return; 325 326 /** 327 * Pointer to see if we need to delete the cache 328 */ 329 $doWeNeedToDeleteTheSideSlotCache = false; 330 331 /** 332 * File creation 333 * 334 * ``` 335 * Page creation may be detected by checking if the file already exists and the revision is false. 336 * ``` 337 * From https://www.dokuwiki.org/devel:event:io_wikipage_write 338 * 339 */ 340 $rev = $data[3]; 341 $filePath = $data[0][0]; 342 $file = File::createFromPath($filePath); 343 if (!$file->exists() && $rev === false) { 344 $doWeNeedToDeleteTheSideSlotCache = true; 345 } 346 347 /** 348 * File deletion 349 * (No content) 350 * 351 * ``` 352 * Page deletion may be detected by checking for empty page content. 353 * 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 * and once to write the new version of the page into the wiki (rev is false) 355 * ``` 356 * From https://www.dokuwiki.org/devel:event:io_wikipage_write 357 */ 358 $append = $data[0][2]; 359 if (!$append) { 360 361 $content = $data[0][1]; 362 if (empty($content) && $rev === false) { 363 // Deletion 364 $doWeNeedToDeleteTheSideSlotCache = true; 365 } 366 367 } 368 369 if ($doWeNeedToDeleteTheSideSlotCache) self::removeSideSlotCache(); 370 371 } 372 373} 374