16ba0f847SSzymon Olewniczak<?php 26ba0f847SSzymon Olewniczak 3*cfdd0e17SAnna Dabrowskause dokuwiki\Logger; 46ba0f847SSzymon Olewniczakuse dokuwiki\Extension\ActionPlugin; 56ba0f847SSzymon Olewniczakuse dokuwiki\Extension\Event; 66ba0f847SSzymon Olewniczakuse dokuwiki\Extension\EventHandler; 76ba0f847SSzymon Olewniczakuse dokuwiki\File\PageFile; 86ba0f847SSzymon Olewniczak 96ba0f847SSzymon Olewniczak/** 106ba0f847SSzymon Olewniczak * DokuWiki Plugin renderrevisions (Action Component) 116ba0f847SSzymon Olewniczak * 126ba0f847SSzymon Olewniczak * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 136ba0f847SSzymon Olewniczak * @author Andreas Gohr <dokuwiki@cosmocode.de> 146ba0f847SSzymon Olewniczak */ 156ba0f847SSzymon Olewniczakclass action_plugin_renderrevisions_save extends ActionPlugin 166ba0f847SSzymon Olewniczak{ 176ba0f847SSzymon Olewniczak /** @var array list of pages that are processed by the plugin */ 186ba0f847SSzymon Olewniczak protected $pages = []; 196ba0f847SSzymon Olewniczak 206ba0f847SSzymon Olewniczak /** @var string|null the current page being saved, used to overwrite the contentchanged check */ 216ba0f847SSzymon Olewniczak protected $current = null; 226ba0f847SSzymon Olewniczak 236ba0f847SSzymon Olewniczak /** @inheritDoc */ 246ba0f847SSzymon Olewniczak public function register(EventHandler $controller) 256ba0f847SSzymon Olewniczak { 266ba0f847SSzymon Olewniczak $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'handleParserCacheUse'); 276ba0f847SSzymon Olewniczak 286ba0f847SSzymon Olewniczak $controller->register_hook( 296ba0f847SSzymon Olewniczak 'RENDERER_CONTENT_POSTPROCESS', 306ba0f847SSzymon Olewniczak 'AFTER', 316ba0f847SSzymon Olewniczak $this, 326ba0f847SSzymon Olewniczak 'handleRenderContent', 336ba0f847SSzymon Olewniczak null, 346ba0f847SSzymon Olewniczak PHP_INT_MAX // other plugins might want to change the content before we see it 356ba0f847SSzymon Olewniczak ); 366ba0f847SSzymon Olewniczak 376ba0f847SSzymon Olewniczak $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handleCommonWikipageSave'); 386ba0f847SSzymon Olewniczak } 396ba0f847SSzymon Olewniczak 406ba0f847SSzymon Olewniczak /** 416ba0f847SSzymon Olewniczak * Event handler for PARSER_CACHE_USE 426ba0f847SSzymon Olewniczak * 436ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:PARSER_CACHE_USE 446ba0f847SSzymon Olewniczak * @param Event $event Event object 456ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 466ba0f847SSzymon Olewniczak * @return void 476ba0f847SSzymon Olewniczak */ 486ba0f847SSzymon Olewniczak public function handleParserCacheUse(Event $event, $param) 496ba0f847SSzymon Olewniczak { 506ba0f847SSzymon Olewniczak $cacheObject = $event->data; 516ba0f847SSzymon Olewniczak 526ba0f847SSzymon Olewniczak if (!$cacheObject->page) return; 536ba0f847SSzymon Olewniczak if ($cacheObject->mode !== 'xhtml') return; 546ba0f847SSzymon Olewniczak 556ba0f847SSzymon Olewniczak // only process pages that match both the skip and match regex 566ba0f847SSzymon Olewniczak 576ba0f847SSzymon Olewniczak $page = $cacheObject->page; 586ba0f847SSzymon Olewniczak try { 596ba0f847SSzymon Olewniczak [$skipRE, $matchRE] = $this->getRegexps(); 606ba0f847SSzymon Olewniczak } catch (\Exception $e) { 616ba0f847SSzymon Olewniczak msg(hsc($e->getMessage()), -1); 626ba0f847SSzymon Olewniczak return; 636ba0f847SSzymon Olewniczak } 646ba0f847SSzymon Olewniczak if ( 656ba0f847SSzymon Olewniczak ($skipRE && preg_match($skipRE, ":$page")) || 666ba0f847SSzymon Olewniczak ($matchRE && !preg_match($matchRE, ":$page")) 676ba0f847SSzymon Olewniczak ) { 686ba0f847SSzymon Olewniczak return; 696ba0f847SSzymon Olewniczak } 706ba0f847SSzymon Olewniczak 716ba0f847SSzymon Olewniczak // remember that this page was processed 726ba0f847SSzymon Olewniczak // This is a somewhat ugly workaround for when text snippets are rendered within the same page. 736ba0f847SSzymon Olewniczak // Those snippets will not have a page context set during cache use event and thus not be processed 746ba0f847SSzymon Olewniczak // later on in the RENDERER_CONTENT_POSTPROCESS event 756ba0f847SSzymon Olewniczak $this->pages[$page] = true; 766ba0f847SSzymon Olewniczak } 776ba0f847SSzymon Olewniczak 786ba0f847SSzymon Olewniczak 796ba0f847SSzymon Olewniczak /** 806ba0f847SSzymon Olewniczak * Event handler for RENDERER_CONTENT_POSTPROCESS 816ba0f847SSzymon Olewniczak * 826ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:RENDERER_CONTENT_POSTPROCESS 836ba0f847SSzymon Olewniczak * @param Event $event Event object 846ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 856ba0f847SSzymon Olewniczak * @return void 866ba0f847SSzymon Olewniczak */ 876ba0f847SSzymon Olewniczak public function handleRenderContent(Event $event, $param) 886ba0f847SSzymon Olewniczak { 896ba0f847SSzymon Olewniczak [$format, $xhtml] = $event->data; 906ba0f847SSzymon Olewniczak if ($format !== 'xhtml') return; 916ba0f847SSzymon Olewniczak 926ba0f847SSzymon Olewniczak // thanks to the $this->pages property we might be able to skip some of those checks, but they don't hurt 936ba0f847SSzymon Olewniczak global $ACT; 946ba0f847SSzymon Olewniczak global $REV; 956ba0f847SSzymon Olewniczak global $DATE_AT; 966ba0f847SSzymon Olewniczak global $ID; 976ba0f847SSzymon Olewniczak global $INFO; 986ba0f847SSzymon Olewniczak if ($ACT !== 'show') return; 996ba0f847SSzymon Olewniczak if ($REV) return; 1006ba0f847SSzymon Olewniczak if ($DATE_AT) return; 1016ba0f847SSzymon Olewniczak if (!$INFO['exists']) return; 1026ba0f847SSzymon Olewniczak if (!$ID) return; 1036ba0f847SSzymon Olewniczak if (!isset($this->pages[$ID])) return; 1046ba0f847SSzymon Olewniczak 1056ba0f847SSzymon Olewniczak // all the above still does not ensure we skip sub renderings, so this is our last resort 1066ba0f847SSzymon Olewniczak if (count(array_filter(debug_backtrace(), fn($t) => $t['function'] === 'p_render')) > 1) return; 1076ba0f847SSzymon Olewniczak 1086ba0f847SSzymon Olewniczak $md5cache = getCacheName($ID, '.renderrevision'); 109ed65e3b8SAndreas Gohr $md5xhtml = $this->getContentHash($xhtml); 1106ba0f847SSzymon Olewniczak 111*cfdd0e17SAnna Dabrowska // depending on config, load storage helper 112*cfdd0e17SAnna Dabrowska /** @var helper_plugin_renderrevisions_storage $storage */ 113*cfdd0e17SAnna Dabrowska $storage = $this->getConf('store') ? plugin_load('helper', 'renderrevisions_storage') : null; 114*cfdd0e17SAnna Dabrowska 1156ba0f847SSzymon Olewniczak // no or outdated MD5 cache, create new one 1166ba0f847SSzymon Olewniczak // this means a new revision of the page has been created naturally 1176ba0f847SSzymon Olewniczak // we store the new render result and are done 1186ba0f847SSzymon Olewniczak if (!file_exists($md5cache) || filemtime(wikiFN($ID)) > filemtime($md5cache)) { 119ed65e3b8SAndreas Gohr file_put_contents($md5cache, $md5xhtml); 120*cfdd0e17SAnna Dabrowska $this->logDebug($ID . ' Wrote render hash cache: path=' . $md5cache . ' hash=' . $md5xhtml); 1216ba0f847SSzymon Olewniczak 122*cfdd0e17SAnna Dabrowska if ($storage) { 1236ba0f847SSzymon Olewniczak $storage->saveRevision($ID, filemtime(wikiFN($ID)), $xhtml); 124*cfdd0e17SAnna Dabrowska $this->logDebug( 125*cfdd0e17SAnna Dabrowska $ID . ' Found new revision and stored render for rev=' . filemtime(wikiFN($ID)) . 126*cfdd0e17SAnna Dabrowska ' in ' . $storage->getFilename($ID, filemtime(wikiFN($ID))) 127*cfdd0e17SAnna Dabrowska ); 128*cfdd0e17SAnna Dabrowska 1296ba0f847SSzymon Olewniczak $storage->cleanUp($ID); 1306ba0f847SSzymon Olewniczak } 1316ba0f847SSzymon Olewniczak 1326ba0f847SSzymon Olewniczak return; 1336ba0f847SSzymon Olewniczak } 1346ba0f847SSzymon Olewniczak 1356ba0f847SSzymon Olewniczak // only act on pages that have not been changed very recently 1366ba0f847SSzymon Olewniczak if (time() - filemtime(wikiFN($ID)) < $this->getConf('maxfrequency')) { 1376ba0f847SSzymon Olewniczak return; 1386ba0f847SSzymon Olewniczak } 1396ba0f847SSzymon Olewniczak 1406ba0f847SSzymon Olewniczak // get the render result as it were when the page was last changed 1416ba0f847SSzymon Olewniczak $oldMd5 = file_get_contents($md5cache); 1426ba0f847SSzymon Olewniczak 1436ba0f847SSzymon Olewniczak // did the rendered content change? 144ed65e3b8SAndreas Gohr if ($oldMd5 === $md5xhtml) { 1456ba0f847SSzymon Olewniczak return; 1466ba0f847SSzymon Olewniczak } 1476ba0f847SSzymon Olewniczak 1486ba0f847SSzymon Olewniczak // time to create a new revision 149*cfdd0e17SAnna Dabrowska $oldPageMtime = filemtime(wikiFN($ID)); 1506ba0f847SSzymon Olewniczak $this->current = $ID; 1516ba0f847SSzymon Olewniczak (new PageFile($ID))->saveWikiText(rawWiki($ID), $this->getLang('summary')); 152*cfdd0e17SAnna Dabrowska 153*cfdd0e17SAnna Dabrowska $this->logDebug($ID . ' Created new wiki revision'); 154*cfdd0e17SAnna Dabrowska 155*cfdd0e17SAnna Dabrowska if ($storage) { 156*cfdd0e17SAnna Dabrowska $diff = $this->getRenderDiff($storage->getRevision($ID, $oldPageMtime), $xhtml); 157*cfdd0e17SAnna Dabrowska $this->logDebug( 158*cfdd0e17SAnna Dabrowska $ID . ' Render diff between stored rev ' . $oldPageMtime . 159*cfdd0e17SAnna Dabrowska ' (' . $storage->getFilename($ID, $oldPageMtime) . ') vs current:' . "\n" . $diff 160*cfdd0e17SAnna Dabrowska ); 161*cfdd0e17SAnna Dabrowska } else { 162*cfdd0e17SAnna Dabrowska $this->logDebug($ID . ' Render diff unavailable: no stored render for rev ' . filemtime(wikiFN($ID))); 163*cfdd0e17SAnna Dabrowska } 164*cfdd0e17SAnna Dabrowska 165*cfdd0e17SAnna Dabrowska 1666ba0f847SSzymon Olewniczak $this->current = null; 1676ba0f847SSzymon Olewniczak } 1686ba0f847SSzymon Olewniczak 1696ba0f847SSzymon Olewniczak 1706ba0f847SSzymon Olewniczak /** 1716ba0f847SSzymon Olewniczak * Event handler for COMMON_WIKIPAGE_SAVE 1726ba0f847SSzymon Olewniczak * 1736ba0f847SSzymon Olewniczak * Overwrite the contentChanged flag to force a new revision even though the content did not change 1746ba0f847SSzymon Olewniczak * 1756ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:COMMON_WIKIPAGE_SAVE 1766ba0f847SSzymon Olewniczak * @param Event $event Event object 1776ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 1786ba0f847SSzymon Olewniczak * @return void 1796ba0f847SSzymon Olewniczak */ 1806ba0f847SSzymon Olewniczak public function handleCommonWikipageSave(Event $event, $param) 1816ba0f847SSzymon Olewniczak { 1826ba0f847SSzymon Olewniczak if ($this->current !== $event->data['id']) return; 1836ba0f847SSzymon Olewniczak $event->data['contentChanged'] = true; 1846ba0f847SSzymon Olewniczak } 1856ba0f847SSzymon Olewniczak 1866ba0f847SSzymon Olewniczak 1876ba0f847SSzymon Olewniczak /** 1886ba0f847SSzymon Olewniczak * Read the skip and match regex from the config 1896ba0f847SSzymon Olewniczak * 1906ba0f847SSzymon Olewniczak * Ensures the regular expressions are valid 1916ba0f847SSzymon Olewniczak * 1926ba0f847SSzymon Olewniczak * @return string[] [$skipRE, $matchRE] 1936ba0f847SSzymon Olewniczak * @throws \Exception if the regular expressions are invalid 1946ba0f847SSzymon Olewniczak */ 1956ba0f847SSzymon Olewniczak public function getRegexps() 1966ba0f847SSzymon Olewniczak { 1976ba0f847SSzymon Olewniczak $skip = $this->getConf('skipRegex'); 1986ba0f847SSzymon Olewniczak $skipRE = ''; 1996ba0f847SSzymon Olewniczak $match = $this->getConf('matchRegex'); 2006ba0f847SSzymon Olewniczak $matchRE = ''; 2016ba0f847SSzymon Olewniczak 2026ba0f847SSzymon Olewniczak if ($skip) { 2036ba0f847SSzymon Olewniczak $skipRE = '/' . $skip . '/'; 2046ba0f847SSzymon Olewniczak if (@preg_match($skipRE, '') === false) { 2056ba0f847SSzymon Olewniczak throw new \Exception('Invalid regular expression in $conf[\'skipRegex\']. ' . preg_last_error_msg()); 2066ba0f847SSzymon Olewniczak } 2076ba0f847SSzymon Olewniczak } 2086ba0f847SSzymon Olewniczak 2096ba0f847SSzymon Olewniczak if ($match) { 2106ba0f847SSzymon Olewniczak $matchRE = '/' . $match . '/'; 2116ba0f847SSzymon Olewniczak if (@preg_match($matchRE, '') === false) { 2126ba0f847SSzymon Olewniczak throw new \Exception('Invalid regular expression in $conf[\'matchRegex\']. ' . preg_last_error_msg()); 2136ba0f847SSzymon Olewniczak } 2146ba0f847SSzymon Olewniczak } 2156ba0f847SSzymon Olewniczak return [$skipRE, $matchRE]; 2166ba0f847SSzymon Olewniczak } 217ed65e3b8SAndreas Gohr 218ed65e3b8SAndreas Gohr /** 219ed65e3b8SAndreas Gohr * Get the hash for the given content 220ed65e3b8SAndreas Gohr * 221ed65e3b8SAndreas Gohr * Strips all whitespace and HTML tags to ensure only real content changes are detected 222ed65e3b8SAndreas Gohr * 223ed65e3b8SAndreas Gohr * @param string $xhtml 224ed65e3b8SAndreas Gohr */ 225*cfdd0e17SAnna Dabrowska protected function getContentHash(string $xhtml): string 226ed65e3b8SAndreas Gohr { 227ed65e3b8SAndreas Gohr return md5(preg_replace('/\s+/', '', strip_tags($xhtml))); 228ed65e3b8SAndreas Gohr } 229*cfdd0e17SAnna Dabrowska 230*cfdd0e17SAnna Dabrowska /** 231*cfdd0e17SAnna Dabrowska * Log a message when debug logging is enabled 232*cfdd0e17SAnna Dabrowska * 233*cfdd0e17SAnna Dabrowska * @param string $message 234*cfdd0e17SAnna Dabrowska * @return void 235*cfdd0e17SAnna Dabrowska */ 236*cfdd0e17SAnna Dabrowska protected function logDebug(string $message) 237*cfdd0e17SAnna Dabrowska { 238*cfdd0e17SAnna Dabrowska if (!$this->getConf('debug')) return; 239*cfdd0e17SAnna Dabrowska $logger = Logger::getInstance('renderrevisions'); 240*cfdd0e17SAnna Dabrowska $logger->log($message); 241*cfdd0e17SAnna Dabrowska } 242*cfdd0e17SAnna Dabrowska 243*cfdd0e17SAnna Dabrowska /** 244*cfdd0e17SAnna Dabrowska * Build a render diff for logging purposes 245*cfdd0e17SAnna Dabrowska * 246*cfdd0e17SAnna Dabrowska * @param string $before 247*cfdd0e17SAnna Dabrowska * @param string $after 248*cfdd0e17SAnna Dabrowska * @return string 249*cfdd0e17SAnna Dabrowska */ 250*cfdd0e17SAnna Dabrowska protected function getRenderDiff(string $before, string $after): string 251*cfdd0e17SAnna Dabrowska { 252*cfdd0e17SAnna Dabrowska if ($before === $after) return 'No render diff (content identical).'; 253*cfdd0e17SAnna Dabrowska 254*cfdd0e17SAnna Dabrowska $Difference = new \Diff( 255*cfdd0e17SAnna Dabrowska explode("\n", $before), 256*cfdd0e17SAnna Dabrowska explode("\n", $after) 257*cfdd0e17SAnna Dabrowska ); 258*cfdd0e17SAnna Dabrowska 259*cfdd0e17SAnna Dabrowska $DiffFormatter = new UnifiedDiffFormatter(); 260*cfdd0e17SAnna Dabrowska return $DiffFormatter->format($Difference); 261*cfdd0e17SAnna Dabrowska } 2626ba0f847SSzymon Olewniczak} 263