16ba0f847SSzymon Olewniczak<?php 26ba0f847SSzymon Olewniczak 36ba0f847SSzymon Olewniczakuse dokuwiki\Extension\ActionPlugin; 46ba0f847SSzymon Olewniczakuse dokuwiki\Extension\Event; 56ba0f847SSzymon Olewniczakuse dokuwiki\Extension\EventHandler; 66ba0f847SSzymon Olewniczakuse dokuwiki\File\PageFile; 76ba0f847SSzymon Olewniczak 86ba0f847SSzymon Olewniczak/** 96ba0f847SSzymon Olewniczak * DokuWiki Plugin renderrevisions (Action Component) 106ba0f847SSzymon Olewniczak * 116ba0f847SSzymon Olewniczak * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 126ba0f847SSzymon Olewniczak * @author Andreas Gohr <dokuwiki@cosmocode.de> 136ba0f847SSzymon Olewniczak */ 146ba0f847SSzymon Olewniczakclass action_plugin_renderrevisions_save extends ActionPlugin 156ba0f847SSzymon Olewniczak{ 166ba0f847SSzymon Olewniczak /** @var array list of pages that are processed by the plugin */ 176ba0f847SSzymon Olewniczak protected $pages = []; 186ba0f847SSzymon Olewniczak 196ba0f847SSzymon Olewniczak /** @var string|null the current page being saved, used to overwrite the contentchanged check */ 206ba0f847SSzymon Olewniczak protected $current = null; 216ba0f847SSzymon Olewniczak 226ba0f847SSzymon Olewniczak /** @inheritDoc */ 236ba0f847SSzymon Olewniczak public function register(EventHandler $controller) 246ba0f847SSzymon Olewniczak { 256ba0f847SSzymon Olewniczak $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'handleParserCacheUse'); 266ba0f847SSzymon Olewniczak 276ba0f847SSzymon Olewniczak $controller->register_hook( 286ba0f847SSzymon Olewniczak 'RENDERER_CONTENT_POSTPROCESS', 296ba0f847SSzymon Olewniczak 'AFTER', 306ba0f847SSzymon Olewniczak $this, 316ba0f847SSzymon Olewniczak 'handleRenderContent', 326ba0f847SSzymon Olewniczak null, 336ba0f847SSzymon Olewniczak PHP_INT_MAX // other plugins might want to change the content before we see it 346ba0f847SSzymon Olewniczak ); 356ba0f847SSzymon Olewniczak 366ba0f847SSzymon Olewniczak $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handleCommonWikipageSave'); 376ba0f847SSzymon Olewniczak } 386ba0f847SSzymon Olewniczak 396ba0f847SSzymon Olewniczak /** 406ba0f847SSzymon Olewniczak * Event handler for PARSER_CACHE_USE 416ba0f847SSzymon Olewniczak * 426ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:PARSER_CACHE_USE 436ba0f847SSzymon Olewniczak * @param Event $event Event object 446ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 456ba0f847SSzymon Olewniczak * @return void 466ba0f847SSzymon Olewniczak */ 476ba0f847SSzymon Olewniczak public function handleParserCacheUse(Event $event, $param) 486ba0f847SSzymon Olewniczak { 496ba0f847SSzymon Olewniczak $cacheObject = $event->data; 506ba0f847SSzymon Olewniczak 516ba0f847SSzymon Olewniczak if (!$cacheObject->page) return; 526ba0f847SSzymon Olewniczak if ($cacheObject->mode !== 'xhtml') return; 536ba0f847SSzymon Olewniczak 546ba0f847SSzymon Olewniczak // only process pages that match both the skip and match regex 556ba0f847SSzymon Olewniczak 566ba0f847SSzymon Olewniczak $page = $cacheObject->page; 576ba0f847SSzymon Olewniczak try { 586ba0f847SSzymon Olewniczak [$skipRE, $matchRE] = $this->getRegexps(); 596ba0f847SSzymon Olewniczak } catch (\Exception $e) { 606ba0f847SSzymon Olewniczak msg(hsc($e->getMessage()), -1); 616ba0f847SSzymon Olewniczak return; 626ba0f847SSzymon Olewniczak } 636ba0f847SSzymon Olewniczak if ( 646ba0f847SSzymon Olewniczak ($skipRE && preg_match($skipRE, ":$page")) || 656ba0f847SSzymon Olewniczak ($matchRE && !preg_match($matchRE, ":$page")) 666ba0f847SSzymon Olewniczak ) { 676ba0f847SSzymon Olewniczak return; 686ba0f847SSzymon Olewniczak } 696ba0f847SSzymon Olewniczak 706ba0f847SSzymon Olewniczak // remember that this page was processed 716ba0f847SSzymon Olewniczak // This is a somewhat ugly workaround for when text snippets are rendered within the same page. 726ba0f847SSzymon Olewniczak // Those snippets will not have a page context set during cache use event and thus not be processed 736ba0f847SSzymon Olewniczak // later on in the RENDERER_CONTENT_POSTPROCESS event 746ba0f847SSzymon Olewniczak $this->pages[$page] = true; 756ba0f847SSzymon Olewniczak } 766ba0f847SSzymon Olewniczak 776ba0f847SSzymon Olewniczak 786ba0f847SSzymon Olewniczak /** 796ba0f847SSzymon Olewniczak * Event handler for RENDERER_CONTENT_POSTPROCESS 806ba0f847SSzymon Olewniczak * 816ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:RENDERER_CONTENT_POSTPROCESS 826ba0f847SSzymon Olewniczak * @param Event $event Event object 836ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 846ba0f847SSzymon Olewniczak * @return void 856ba0f847SSzymon Olewniczak */ 866ba0f847SSzymon Olewniczak public function handleRenderContent(Event $event, $param) 876ba0f847SSzymon Olewniczak { 886ba0f847SSzymon Olewniczak [$format, $xhtml] = $event->data; 896ba0f847SSzymon Olewniczak if ($format !== 'xhtml') return; 906ba0f847SSzymon Olewniczak 916ba0f847SSzymon Olewniczak // thanks to the $this->pages property we might be able to skip some of those checks, but they don't hurt 926ba0f847SSzymon Olewniczak global $ACT; 936ba0f847SSzymon Olewniczak global $REV; 946ba0f847SSzymon Olewniczak global $DATE_AT; 956ba0f847SSzymon Olewniczak global $ID; 966ba0f847SSzymon Olewniczak global $INFO; 976ba0f847SSzymon Olewniczak if ($ACT !== 'show') return; 986ba0f847SSzymon Olewniczak if ($REV) return; 996ba0f847SSzymon Olewniczak if ($DATE_AT) return; 1006ba0f847SSzymon Olewniczak if (!$INFO['exists']) return; 1016ba0f847SSzymon Olewniczak if (!$ID) return; 1026ba0f847SSzymon Olewniczak if (!isset($this->pages[$ID])) return; 1036ba0f847SSzymon Olewniczak 1046ba0f847SSzymon Olewniczak // all the above still does not ensure we skip sub renderings, so this is our last resort 1056ba0f847SSzymon Olewniczak if (count(array_filter(debug_backtrace(), fn($t) => $t['function'] === 'p_render')) > 1) return; 1066ba0f847SSzymon Olewniczak 1076ba0f847SSzymon Olewniczak $md5cache = getCacheName($ID, '.renderrevision'); 108*ed65e3b8SAndreas Gohr $md5xhtml = $this->getContentHash($xhtml); 1096ba0f847SSzymon Olewniczak 1106ba0f847SSzymon Olewniczak // no or outdated MD5 cache, create new one 1116ba0f847SSzymon Olewniczak // this means a new revision of the page has been created naturally 1126ba0f847SSzymon Olewniczak // we store the new render result and are done 1136ba0f847SSzymon Olewniczak if (!file_exists($md5cache) || filemtime(wikiFN($ID)) > filemtime($md5cache)) { 114*ed65e3b8SAndreas Gohr file_put_contents($md5cache, $md5xhtml); 1156ba0f847SSzymon Olewniczak 1166ba0f847SSzymon Olewniczak if ($this->getConf('store')) { 1176ba0f847SSzymon Olewniczak /** @var helper_plugin_renderrevisions_storage $storage */ 1186ba0f847SSzymon Olewniczak $storage = plugin_load('helper', 'renderrevisions_storage'); 1196ba0f847SSzymon Olewniczak $storage->saveRevision($ID, filemtime(wikiFN($ID)), $xhtml); 1206ba0f847SSzymon Olewniczak $storage->cleanUp($ID); 1216ba0f847SSzymon Olewniczak } 1226ba0f847SSzymon Olewniczak 1236ba0f847SSzymon Olewniczak return; 1246ba0f847SSzymon Olewniczak } 1256ba0f847SSzymon Olewniczak 1266ba0f847SSzymon Olewniczak // only act on pages that have not been changed very recently 1276ba0f847SSzymon Olewniczak if (time() - filemtime(wikiFN($ID)) < $this->getConf('maxfrequency')) { 1286ba0f847SSzymon Olewniczak return; 1296ba0f847SSzymon Olewniczak } 1306ba0f847SSzymon Olewniczak 1316ba0f847SSzymon Olewniczak // get the render result as it were when the page was last changed 1326ba0f847SSzymon Olewniczak $oldMd5 = file_get_contents($md5cache); 1336ba0f847SSzymon Olewniczak 1346ba0f847SSzymon Olewniczak // did the rendered content change? 135*ed65e3b8SAndreas Gohr if ($oldMd5 === $md5xhtml) { 1366ba0f847SSzymon Olewniczak return; 1376ba0f847SSzymon Olewniczak } 1386ba0f847SSzymon Olewniczak 1396ba0f847SSzymon Olewniczak // time to create a new revision 1406ba0f847SSzymon Olewniczak $this->current = $ID; 1416ba0f847SSzymon Olewniczak (new PageFile($ID))->saveWikiText(rawWiki($ID), $this->getLang('summary')); 1426ba0f847SSzymon Olewniczak $this->current = null; 1436ba0f847SSzymon Olewniczak } 1446ba0f847SSzymon Olewniczak 1456ba0f847SSzymon Olewniczak 1466ba0f847SSzymon Olewniczak /** 1476ba0f847SSzymon Olewniczak * Event handler for COMMON_WIKIPAGE_SAVE 1486ba0f847SSzymon Olewniczak * 1496ba0f847SSzymon Olewniczak * Overwrite the contentChanged flag to force a new revision even though the content did not change 1506ba0f847SSzymon Olewniczak * 1516ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:COMMON_WIKIPAGE_SAVE 1526ba0f847SSzymon Olewniczak * @param Event $event Event object 1536ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 1546ba0f847SSzymon Olewniczak * @return void 1556ba0f847SSzymon Olewniczak */ 1566ba0f847SSzymon Olewniczak public function handleCommonWikipageSave(Event $event, $param) 1576ba0f847SSzymon Olewniczak { 1586ba0f847SSzymon Olewniczak if ($this->current !== $event->data['id']) return; 1596ba0f847SSzymon Olewniczak $event->data['contentChanged'] = true; 1606ba0f847SSzymon Olewniczak } 1616ba0f847SSzymon Olewniczak 1626ba0f847SSzymon Olewniczak 1636ba0f847SSzymon Olewniczak /** 1646ba0f847SSzymon Olewniczak * Read the skip and match regex from the config 1656ba0f847SSzymon Olewniczak * 1666ba0f847SSzymon Olewniczak * Ensures the regular expressions are valid 1676ba0f847SSzymon Olewniczak * 1686ba0f847SSzymon Olewniczak * @return string[] [$skipRE, $matchRE] 1696ba0f847SSzymon Olewniczak * @throws \Exception if the regular expressions are invalid 1706ba0f847SSzymon Olewniczak */ 1716ba0f847SSzymon Olewniczak public function getRegexps() 1726ba0f847SSzymon Olewniczak { 1736ba0f847SSzymon Olewniczak $skip = $this->getConf('skipRegex'); 1746ba0f847SSzymon Olewniczak $skipRE = ''; 1756ba0f847SSzymon Olewniczak $match = $this->getConf('matchRegex'); 1766ba0f847SSzymon Olewniczak $matchRE = ''; 1776ba0f847SSzymon Olewniczak 1786ba0f847SSzymon Olewniczak if ($skip) { 1796ba0f847SSzymon Olewniczak $skipRE = '/' . $skip . '/'; 1806ba0f847SSzymon Olewniczak if (@preg_match($skipRE, '') === false) { 1816ba0f847SSzymon Olewniczak throw new \Exception('Invalid regular expression in $conf[\'skipRegex\']. ' . preg_last_error_msg()); 1826ba0f847SSzymon Olewniczak } 1836ba0f847SSzymon Olewniczak } 1846ba0f847SSzymon Olewniczak 1856ba0f847SSzymon Olewniczak if ($match) { 1866ba0f847SSzymon Olewniczak $matchRE = '/' . $match . '/'; 1876ba0f847SSzymon Olewniczak if (@preg_match($matchRE, '') === false) { 1886ba0f847SSzymon Olewniczak throw new \Exception('Invalid regular expression in $conf[\'matchRegex\']. ' . preg_last_error_msg()); 1896ba0f847SSzymon Olewniczak } 1906ba0f847SSzymon Olewniczak } 1916ba0f847SSzymon Olewniczak return [$skipRE, $matchRE]; 1926ba0f847SSzymon Olewniczak } 193*ed65e3b8SAndreas Gohr 194*ed65e3b8SAndreas Gohr /** 195*ed65e3b8SAndreas Gohr * Get the hash for the given content 196*ed65e3b8SAndreas Gohr * 197*ed65e3b8SAndreas Gohr * Strips all whitespace and HTML tags to ensure only real content changes are detected 198*ed65e3b8SAndreas Gohr * 199*ed65e3b8SAndreas Gohr * @param string $xhtml 200*ed65e3b8SAndreas Gohr */ 201*ed65e3b8SAndreas Gohr protected function getContentHash($xhtml): string 202*ed65e3b8SAndreas Gohr { 203*ed65e3b8SAndreas Gohr return md5(preg_replace('/\s+/', '', strip_tags($xhtml))); 204*ed65e3b8SAndreas Gohr } 2056ba0f847SSzymon Olewniczak} 206