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_save 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 * Event handler for RENDERER_CONTENT_POSTPROCESS 80 * 81 * @see https://www.dokuwiki.org/devel:event:RENDERER_CONTENT_POSTPROCESS 82 * @param Event $event Event object 83 * @param mixed $param optional parameter passed when event was registered 84 * @return void 85 */ 86 public function handleRenderContent(Event $event, $param) 87 { 88 [$format, $xhtml] = $event->data; 89 if ($format !== 'xhtml') return; 90 91 // thanks to the $this->pages property we might be able to skip some of those checks, but they don't hurt 92 global $ACT; 93 global $REV; 94 global $DATE_AT; 95 global $ID; 96 global $INFO; 97 if ($ACT !== 'show') return; 98 if ($REV) return; 99 if ($DATE_AT) return; 100 if (!$INFO['exists']) return; 101 if (!$ID) return; 102 if (!isset($this->pages[$ID])) return; 103 104 // all the above still does not ensure we skip sub renderings, so this is our last resort 105 if (count(array_filter(debug_backtrace(), fn($t) => $t['function'] === 'p_render')) > 1) return; 106 107 $md5cache = getCacheName($ID, '.renderrevision'); 108 $md5xhtml = $this->getContentHash($xhtml); 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, $md5xhtml); 115 116 if ($this->getConf('store')) { 117 /** @var helper_plugin_renderrevisions_storage $storage */ 118 $storage = plugin_load('helper', 'renderrevisions_storage'); 119 $storage->saveRevision($ID, filemtime(wikiFN($ID)), $xhtml); 120 $storage->cleanUp($ID); 121 } 122 123 return; 124 } 125 126 // only act on pages that have not been changed very recently 127 if (time() - filemtime(wikiFN($ID)) < $this->getConf('maxfrequency')) { 128 return; 129 } 130 131 // get the render result as it were when the page was last changed 132 $oldMd5 = file_get_contents($md5cache); 133 134 // did the rendered content change? 135 if ($oldMd5 === $md5xhtml) { 136 return; 137 } 138 139 // time to create a new revision 140 $this->current = $ID; 141 (new PageFile($ID))->saveWikiText(rawWiki($ID), $this->getLang('summary')); 142 $this->current = null; 143 } 144 145 146 /** 147 * Event handler for COMMON_WIKIPAGE_SAVE 148 * 149 * Overwrite the contentChanged flag to force a new revision even though the content did not change 150 * 151 * @see https://www.dokuwiki.org/devel:event:COMMON_WIKIPAGE_SAVE 152 * @param Event $event Event object 153 * @param mixed $param optional parameter passed when event was registered 154 * @return void 155 */ 156 public function handleCommonWikipageSave(Event $event, $param) 157 { 158 if ($this->current !== $event->data['id']) return; 159 $event->data['contentChanged'] = true; 160 } 161 162 163 /** 164 * Read the skip and match regex from the config 165 * 166 * Ensures the regular expressions are valid 167 * 168 * @return string[] [$skipRE, $matchRE] 169 * @throws \Exception if the regular expressions are invalid 170 */ 171 public function getRegexps() 172 { 173 $skip = $this->getConf('skipRegex'); 174 $skipRE = ''; 175 $match = $this->getConf('matchRegex'); 176 $matchRE = ''; 177 178 if ($skip) { 179 $skipRE = '/' . $skip . '/'; 180 if (@preg_match($skipRE, '') === false) { 181 throw new \Exception('Invalid regular expression in $conf[\'skipRegex\']. ' . preg_last_error_msg()); 182 } 183 } 184 185 if ($match) { 186 $matchRE = '/' . $match . '/'; 187 if (@preg_match($matchRE, '') === false) { 188 throw new \Exception('Invalid regular expression in $conf[\'matchRegex\']. ' . preg_last_error_msg()); 189 } 190 } 191 return [$skipRE, $matchRE]; 192 } 193 194 /** 195 * Get the hash for the given content 196 * 197 * Strips all whitespace and HTML tags to ensure only real content changes are detected 198 * 199 * @param string $xhtml 200 */ 201 protected function getContentHash($xhtml): string 202 { 203 return md5(preg_replace('/\s+/', '', strip_tags($xhtml))); 204 } 205} 206