1*6ba0f847SSzymon Olewniczak<?php 2*6ba0f847SSzymon Olewniczak 3*6ba0f847SSzymon Olewniczakuse dokuwiki\Extension\ActionPlugin; 4*6ba0f847SSzymon Olewniczakuse dokuwiki\Extension\Event; 5*6ba0f847SSzymon Olewniczakuse dokuwiki\Extension\EventHandler; 6*6ba0f847SSzymon Olewniczakuse dokuwiki\File\PageFile; 7*6ba0f847SSzymon Olewniczak 8*6ba0f847SSzymon Olewniczak/** 9*6ba0f847SSzymon Olewniczak * DokuWiki Plugin renderrevisions (Action Component) 10*6ba0f847SSzymon Olewniczak * 11*6ba0f847SSzymon Olewniczak * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 12*6ba0f847SSzymon Olewniczak * @author Andreas Gohr <dokuwiki@cosmocode.de> 13*6ba0f847SSzymon Olewniczak */ 14*6ba0f847SSzymon Olewniczakclass action_plugin_renderrevisions_save extends ActionPlugin 15*6ba0f847SSzymon Olewniczak{ 16*6ba0f847SSzymon Olewniczak /** @var array list of pages that are processed by the plugin */ 17*6ba0f847SSzymon Olewniczak protected $pages = []; 18*6ba0f847SSzymon Olewniczak 19*6ba0f847SSzymon Olewniczak /** @var string|null the current page being saved, used to overwrite the contentchanged check */ 20*6ba0f847SSzymon Olewniczak protected $current = null; 21*6ba0f847SSzymon Olewniczak 22*6ba0f847SSzymon Olewniczak /** @inheritDoc */ 23*6ba0f847SSzymon Olewniczak public function register(EventHandler $controller) 24*6ba0f847SSzymon Olewniczak { 25*6ba0f847SSzymon Olewniczak $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'handleParserCacheUse'); 26*6ba0f847SSzymon Olewniczak 27*6ba0f847SSzymon Olewniczak $controller->register_hook( 28*6ba0f847SSzymon Olewniczak 'RENDERER_CONTENT_POSTPROCESS', 29*6ba0f847SSzymon Olewniczak 'AFTER', 30*6ba0f847SSzymon Olewniczak $this, 31*6ba0f847SSzymon Olewniczak 'handleRenderContent', 32*6ba0f847SSzymon Olewniczak null, 33*6ba0f847SSzymon Olewniczak PHP_INT_MAX // other plugins might want to change the content before we see it 34*6ba0f847SSzymon Olewniczak ); 35*6ba0f847SSzymon Olewniczak 36*6ba0f847SSzymon Olewniczak $controller->register_hook('COMMON_WIKIPAGE_SAVE', 'BEFORE', $this, 'handleCommonWikipageSave'); 37*6ba0f847SSzymon Olewniczak } 38*6ba0f847SSzymon Olewniczak 39*6ba0f847SSzymon Olewniczak /** 40*6ba0f847SSzymon Olewniczak * Event handler for PARSER_CACHE_USE 41*6ba0f847SSzymon Olewniczak * 42*6ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:PARSER_CACHE_USE 43*6ba0f847SSzymon Olewniczak * @param Event $event Event object 44*6ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 45*6ba0f847SSzymon Olewniczak * @return void 46*6ba0f847SSzymon Olewniczak */ 47*6ba0f847SSzymon Olewniczak public function handleParserCacheUse(Event $event, $param) 48*6ba0f847SSzymon Olewniczak { 49*6ba0f847SSzymon Olewniczak $cacheObject = $event->data; 50*6ba0f847SSzymon Olewniczak 51*6ba0f847SSzymon Olewniczak if (!$cacheObject->page) return; 52*6ba0f847SSzymon Olewniczak if ($cacheObject->mode !== 'xhtml') return; 53*6ba0f847SSzymon Olewniczak 54*6ba0f847SSzymon Olewniczak // only process pages that match both the skip and match regex 55*6ba0f847SSzymon Olewniczak 56*6ba0f847SSzymon Olewniczak $page = $cacheObject->page; 57*6ba0f847SSzymon Olewniczak try { 58*6ba0f847SSzymon Olewniczak [$skipRE, $matchRE] = $this->getRegexps(); 59*6ba0f847SSzymon Olewniczak } catch (\Exception $e) { 60*6ba0f847SSzymon Olewniczak msg(hsc($e->getMessage()), -1); 61*6ba0f847SSzymon Olewniczak return; 62*6ba0f847SSzymon Olewniczak } 63*6ba0f847SSzymon Olewniczak if ( 64*6ba0f847SSzymon Olewniczak ($skipRE && preg_match($skipRE, ":$page")) || 65*6ba0f847SSzymon Olewniczak ($matchRE && !preg_match($matchRE, ":$page")) 66*6ba0f847SSzymon Olewniczak ) { 67*6ba0f847SSzymon Olewniczak return; 68*6ba0f847SSzymon Olewniczak } 69*6ba0f847SSzymon Olewniczak 70*6ba0f847SSzymon Olewniczak // remember that this page was processed 71*6ba0f847SSzymon Olewniczak // This is a somewhat ugly workaround for when text snippets are rendered within the same page. 72*6ba0f847SSzymon Olewniczak // Those snippets will not have a page context set during cache use event and thus not be processed 73*6ba0f847SSzymon Olewniczak // later on in the RENDERER_CONTENT_POSTPROCESS event 74*6ba0f847SSzymon Olewniczak $this->pages[$page] = true; 75*6ba0f847SSzymon Olewniczak } 76*6ba0f847SSzymon Olewniczak 77*6ba0f847SSzymon Olewniczak 78*6ba0f847SSzymon Olewniczak /** 79*6ba0f847SSzymon Olewniczak * Event handler for RENDERER_CONTENT_POSTPROCESS 80*6ba0f847SSzymon Olewniczak * 81*6ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:RENDERER_CONTENT_POSTPROCESS 82*6ba0f847SSzymon Olewniczak * @param Event $event Event object 83*6ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 84*6ba0f847SSzymon Olewniczak * @return void 85*6ba0f847SSzymon Olewniczak */ 86*6ba0f847SSzymon Olewniczak public function handleRenderContent(Event $event, $param) 87*6ba0f847SSzymon Olewniczak { 88*6ba0f847SSzymon Olewniczak [$format, $xhtml] = $event->data; 89*6ba0f847SSzymon Olewniczak if ($format !== 'xhtml') return; 90*6ba0f847SSzymon Olewniczak 91*6ba0f847SSzymon Olewniczak // thanks to the $this->pages property we might be able to skip some of those checks, but they don't hurt 92*6ba0f847SSzymon Olewniczak global $ACT; 93*6ba0f847SSzymon Olewniczak global $REV; 94*6ba0f847SSzymon Olewniczak global $DATE_AT; 95*6ba0f847SSzymon Olewniczak global $ID; 96*6ba0f847SSzymon Olewniczak global $INFO; 97*6ba0f847SSzymon Olewniczak if ($ACT !== 'show') return; 98*6ba0f847SSzymon Olewniczak if ($REV) return; 99*6ba0f847SSzymon Olewniczak if ($DATE_AT) return; 100*6ba0f847SSzymon Olewniczak if (!$INFO['exists']) return; 101*6ba0f847SSzymon Olewniczak if (!$ID) return; 102*6ba0f847SSzymon Olewniczak if (!isset($this->pages[$ID])) return; 103*6ba0f847SSzymon Olewniczak 104*6ba0f847SSzymon Olewniczak // all the above still does not ensure we skip sub renderings, so this is our last resort 105*6ba0f847SSzymon Olewniczak if (count(array_filter(debug_backtrace(), fn($t) => $t['function'] === 'p_render')) > 1) return; 106*6ba0f847SSzymon Olewniczak 107*6ba0f847SSzymon Olewniczak $md5cache = getCacheName($ID, '.renderrevision'); 108*6ba0f847SSzymon Olewniczak 109*6ba0f847SSzymon Olewniczak // no or outdated MD5 cache, create new one 110*6ba0f847SSzymon Olewniczak // this means a new revision of the page has been created naturally 111*6ba0f847SSzymon Olewniczak // we store the new render result and are done 112*6ba0f847SSzymon Olewniczak if (!file_exists($md5cache) || filemtime(wikiFN($ID)) > filemtime($md5cache)) { 113*6ba0f847SSzymon Olewniczak file_put_contents($md5cache, md5($xhtml)); 114*6ba0f847SSzymon Olewniczak 115*6ba0f847SSzymon Olewniczak if($this->getConf('store')) { 116*6ba0f847SSzymon Olewniczak /** @var helper_plugin_renderrevisions_storage $storage */ 117*6ba0f847SSzymon Olewniczak $storage = plugin_load('helper', 'renderrevisions_storage'); 118*6ba0f847SSzymon Olewniczak $storage->saveRevision($ID, filemtime(wikiFN($ID)), $xhtml); 119*6ba0f847SSzymon Olewniczak $storage->cleanUp($ID); 120*6ba0f847SSzymon Olewniczak } 121*6ba0f847SSzymon Olewniczak 122*6ba0f847SSzymon Olewniczak return; 123*6ba0f847SSzymon Olewniczak } 124*6ba0f847SSzymon Olewniczak 125*6ba0f847SSzymon Olewniczak // only act on pages that have not been changed very recently 126*6ba0f847SSzymon Olewniczak if (time() - filemtime(wikiFN($ID)) < $this->getConf('maxfrequency')) { 127*6ba0f847SSzymon Olewniczak return; 128*6ba0f847SSzymon Olewniczak } 129*6ba0f847SSzymon Olewniczak 130*6ba0f847SSzymon Olewniczak // get the render result as it were when the page was last changed 131*6ba0f847SSzymon Olewniczak $oldMd5 = file_get_contents($md5cache); 132*6ba0f847SSzymon Olewniczak 133*6ba0f847SSzymon Olewniczak // did the rendered content change? 134*6ba0f847SSzymon Olewniczak if ($oldMd5 === md5($xhtml)) { 135*6ba0f847SSzymon Olewniczak return; 136*6ba0f847SSzymon Olewniczak } 137*6ba0f847SSzymon Olewniczak 138*6ba0f847SSzymon Olewniczak // time to create a new revision 139*6ba0f847SSzymon Olewniczak $this->current = $ID; 140*6ba0f847SSzymon Olewniczak (new PageFile($ID))->saveWikiText(rawWiki($ID), $this->getLang('summary')); 141*6ba0f847SSzymon Olewniczak $this->current = null; 142*6ba0f847SSzymon Olewniczak } 143*6ba0f847SSzymon Olewniczak 144*6ba0f847SSzymon Olewniczak 145*6ba0f847SSzymon Olewniczak /** 146*6ba0f847SSzymon Olewniczak * Event handler for COMMON_WIKIPAGE_SAVE 147*6ba0f847SSzymon Olewniczak * 148*6ba0f847SSzymon Olewniczak * Overwrite the contentChanged flag to force a new revision even though the content did not change 149*6ba0f847SSzymon Olewniczak * 150*6ba0f847SSzymon Olewniczak * @see https://www.dokuwiki.org/devel:event:COMMON_WIKIPAGE_SAVE 151*6ba0f847SSzymon Olewniczak * @param Event $event Event object 152*6ba0f847SSzymon Olewniczak * @param mixed $param optional parameter passed when event was registered 153*6ba0f847SSzymon Olewniczak * @return void 154*6ba0f847SSzymon Olewniczak */ 155*6ba0f847SSzymon Olewniczak public function handleCommonWikipageSave(Event $event, $param) 156*6ba0f847SSzymon Olewniczak { 157*6ba0f847SSzymon Olewniczak if ($this->current !== $event->data['id']) return; 158*6ba0f847SSzymon Olewniczak $event->data['contentChanged'] = true; 159*6ba0f847SSzymon Olewniczak } 160*6ba0f847SSzymon Olewniczak 161*6ba0f847SSzymon Olewniczak 162*6ba0f847SSzymon Olewniczak /** 163*6ba0f847SSzymon Olewniczak * Read the skip and match regex from the config 164*6ba0f847SSzymon Olewniczak * 165*6ba0f847SSzymon Olewniczak * Ensures the regular expressions are valid 166*6ba0f847SSzymon Olewniczak * 167*6ba0f847SSzymon Olewniczak * @return string[] [$skipRE, $matchRE] 168*6ba0f847SSzymon Olewniczak * @throws \Exception if the regular expressions are invalid 169*6ba0f847SSzymon Olewniczak */ 170*6ba0f847SSzymon Olewniczak public function getRegexps() 171*6ba0f847SSzymon Olewniczak { 172*6ba0f847SSzymon Olewniczak $skip = $this->getConf('skipRegex'); 173*6ba0f847SSzymon Olewniczak $skipRE = ''; 174*6ba0f847SSzymon Olewniczak $match = $this->getConf('matchRegex'); 175*6ba0f847SSzymon Olewniczak $matchRE = ''; 176*6ba0f847SSzymon Olewniczak 177*6ba0f847SSzymon Olewniczak if ($skip) { 178*6ba0f847SSzymon Olewniczak $skipRE = '/' . $skip . '/'; 179*6ba0f847SSzymon Olewniczak if (@preg_match($skipRE, '') === false) { 180*6ba0f847SSzymon Olewniczak throw new \Exception('Invalid regular expression in $conf[\'skipRegex\']. ' . preg_last_error_msg()); 181*6ba0f847SSzymon Olewniczak } 182*6ba0f847SSzymon Olewniczak } 183*6ba0f847SSzymon Olewniczak 184*6ba0f847SSzymon Olewniczak if ($match) { 185*6ba0f847SSzymon Olewniczak $matchRE = '/' . $match . '/'; 186*6ba0f847SSzymon Olewniczak if (@preg_match($matchRE, '') === false) { 187*6ba0f847SSzymon Olewniczak throw new \Exception('Invalid regular expression in $conf[\'matchRegex\']. ' . preg_last_error_msg()); 188*6ba0f847SSzymon Olewniczak } 189*6ba0f847SSzymon Olewniczak } 190*6ba0f847SSzymon Olewniczak return [$skipRE, $matchRE]; 191*6ba0f847SSzymon Olewniczak } 192*6ba0f847SSzymon Olewniczak} 193