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