1<?php 2 3/** 4 * DokuWiki Plugin doxycode (Action Component) 5 * 6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7 * @author Lukas Probsthain <lukas.probsthain@gmail.com> 8 */ 9 10use dokuwiki\Extension\ActionPlugin; 11use dokuwiki\HTTP\DokuHTTPClient; 12use dokuwiki\Extension\Event; 13use dokuwiki\Cache\Cache; 14use dokuwiki\Extension\EventHandler; 15 16/** 17 * Class action_plugin_doxycode 18 * 19 * This action component of the doxygen plugin handles the download of remote tag files, 20 * build job/task execution, cache invalidation, and ajax calls for checking the job/task 21 * execution and dynamic loading of finished code snippets. 22 * 23 * @author Lukas Probsthain <lukas.probsthain@gmail.com> 24 */ 25class action_plugin_doxycode extends ActionPlugin 26{ 27 public function register(EventHandler $controller) 28 { 29 $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'loadRemoteTagFiles'); 30 $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'renderDoxyCodeSnippets'); 31 $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'beforeParserCacheUse'); 32 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'ajaxCall'); 33 $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'insertTagButton'); 34 $controller->register_hook('RPC_CALL_ADD', 'AFTER', $this, 'addRpcMethod'); 35 } 36 37 /** 38 * Download remote doxygen tag files and place the in the tag file directory. 39 * 40 * Remote tag files are tag files that are publicly hosted on another website. 41 * This task runner hook gets the tag file configuration for remote tag files and checks 42 * if it is time to check the remote tag file again. 43 * 44 * If the time threshold is reached for checking again it tries to download the tag file again, 45 * updates the 'last_update' timestamp in the configuration, and then checks if we have an updated 46 * tag file by comparing the md5 of the cached tag file. 47 * 48 * The configuration with the updated 'last_update' timestamp is saved without modifying the mtime 49 * of the configuration because the configuration file is listed as a file dependency among the used tag files 50 * for the cached snippets. We only want to invalidate the cached snippets if the configuration really changes 51 * or a new tag file is available. 52 */ 53 public function loadRemoteTagFiles(Event $event, $param) 54 { 55 // get the tag files from the helper 56 57 /** @var helper_plugin_doxycode_tagmanager $tagmanager */ 58 $tagmanager = plugin_load('helper', 'doxycode_tagmanager'); 59 60 // load complete tag file configuration 61 // we need this since we'll save the configuration later 62 // if we use the filtered configuration for saving we would remove all other elements 63 $tag_config = $tagmanager->loadTagFileConfig(); 64 65 // only try to download a tag file if it is a remote file and enabled! 66 // TODO: we could also use a filter function for filtering remote configurations with overdue updates 67 $filtered_config = $tagmanager->filterConfig($tag_config, ['isConfigEnabled','isValidRemoteConfig']); 68 69 // loop over all tag file configurations 70 foreach ($filtered_config as $key => &$conf) { 71 $now = time(); 72 $timestamp = $conf['last_update'] ? $conf['last_update'] : 0; 73 $update_period = $conf['update_period'] ? $conf['update_period'] : $this->getConf('update_period'); 74 $filename = $tagmanager->getTagFileDir() . $key . '.xml'; 75 76 // only try to update every $update_period amount of seconds 77 if ($now - $update_period >= $timestamp) { 78 // only process one thing per task runner run 79 $event->stopPropagation(); 80 $event->preventDefault(); 81 82 $tag_config[$key]['last_update'] = $now; 83 // save the new timestamp - regardless of the success of the rest 84 // on connection failures, we don't want to run the task runner constantly on failures! 85 // true: do not update the mtime of the configuration! 86 // if a new tag file is available we invalidate the cache if this tag file was used in a page! 87 $tagmanager->saveTagFileConfig($tag_config, true); 88 89 $exists = false; // if the file does not exist - just write now! 90 $cachedHash = ''; // this is used to check if we really have a new file 91 92 // first read the old file and free memory so we do not exeed the limit! 93 if ($cachedContent = @file_get_contents($filename)) { 94 $exists = true; 95 96 $cachedHash = md5($cachedContent); 97 98 unset($cachedContent); 99 } 100 101 // it's time to reload the tag file 102 $http = new DokuHTTPClient(); 103 $url = $conf['remote_url']; 104 if (!$http->get($url)) { 105 $error = 'could not get ' . hsc($url) . ', HTTP status ' . $http->status . '. '; 106 throw new Exception($error); 107 } 108 109 // here we have the new content 110 $newContent = $http->resp_body; 111 $newHash = md5($newContent); 112 113 if (!$exists || $cachedHash !== $newHash) { 114 // save the new tag file 115 file_put_contents($filename, $newContent); 116 } 117 118 119 return; // we only ever want to run one file! 120 } 121 } 122 } 123 124 public function renderDoxyCodeSnippets(Event $event) 125 { 126 global $ID; 127 128 /** @var helper_plugin_doxycode_buildmanager $buildmanager */ 129 $buildmanager = plugin_load('helper', 'doxycode_buildmanager'); 130 131 // TODO: instead of counting the executions in $iterations we could directly obtain 132 // a specific amount of tasks from getBuildTasks 133 $tasks = $buildmanager->getBuildTasks(); 134 135 if (sizeof($tasks) > 0) { 136 $event->stopPropagation(); 137 $event->preventDefault(); 138 139 $iterations = 0; 140 // TODO: we should implement a maximum amount of tasks to run in one task runner instance! 141 foreach ($tasks as $task) { 142 $iterations++; 143 144 if ($iterations > $this->getConf('runner_max_tasks')) { 145 return; 146 } 147 148 if (!$buildmanager->runTask($task['TaskID'])) { 149 // if we couldn't build abort the task runner 150 // this might happen if another instance is already running 151 return; 152 } 153 } 154 } 155 } 156 157 public function beforeParserCacheUse(Event $event) 158 { 159 global $ID; 160 $cache = $event->data; 161 if (isset($cache->mode) && ($cache->mode == 'xhtml')) { 162 // load doxycode meta that includes the used tag files and the cache files for the snippets 163 $doxycode_meta = p_get_metadata($ID, 'doxycode'); 164 165 if ($doxycode_meta == null) { 166 // doxycode was not used in this page! 167 return; 168 } 169 170 $depends = []; 171 172 // if the tagfiles were updated in the background, we need to rebuild the snippets 173 // in this case we first need to invalidate the page cache 174 $tagfiles = $doxycode_meta['tagfiles']; 175 176 if ($tagfiles != null) { 177 // transform the list of tag files into an array that can be used by the helper 178 // ['tag_name' => []]: empty array as value since we do not have a loaded tag configuration here! 179 $tag_config = array_fill_keys($tagfiles, []); 180 181 /** @var helper_plugin_doxycode $helper */ 182 $helper = plugin_load('helper', 'doxycode'); 183 184 // transform the tag names to full file paths 185 $helper->getTagFiles($depends, $tag_config); 186 }; 187 188 // load the PHP file dependencies 189 // if any file was changed, we want to do a reload 190 // the other dependencies (cache files) might not be enough, 191 // if e.g. the way we generate the hash names change 192 // this might happen if the old cache files still exist and the meta data was not updated 193 $helper->getPHPFileDependencies($depends); 194 195 // add these to the dependency list 196 if (!empty($depends) && isset($depends['files'])) { 197 $this->addDependencies($cache, $depends['files']); 198 } 199 200 // if the XML cache is not existent we should check if the build is scheduled in the main syntax component 201 $cache_files = $doxycode_meta['xml_cachefiles']; 202 203 foreach ($cache_files as $cacheID) { 204 $cache_name = getCacheName($cacheID, ".xml"); 205 206 if (!@file_exists($cache_name)) { 207 $event->preventDefault(); 208 $event->stopPropagation(); 209 $event->result = false; 210 } 211 212 $this->addDependencies($cache, [$cache_name]); 213 } 214 215 // if the HTML cache is not existent we should parse 216 // the XML in the main syntax component before the page loads 217 $cache_files = $doxycode_meta['html_cachefiles']; 218 219 foreach ($cache_files as $cacheID) { 220 $cache_name = getCacheName($cacheID, ".html"); 221 222 if (!@file_exists($cache_name)) { 223 $event->preventDefault(); 224 $event->stopPropagation(); 225 $event->result = false; 226 } 227 228 $this->addDependencies($cache, [$cache_name]); 229 } 230 } 231 } 232 233 234 /** 235 * Add extra dependencies to the cache 236 * 237 * copied from changes plugin 238 */ 239 protected function addDependencies($cache, $depends) 240 { 241 // Prevent "Warning: in_array() expects parameter 2 to be array, null given" 242 if (!is_array($cache->depends)) { 243 $cache->depends = []; 244 } 245 if (!array_key_exists('files', $cache->depends)) { 246 $cache->depends['files'] = []; 247 } 248 249 foreach ($depends as $file) { 250 if (!in_array($file, $cache->depends['files']) && @file_exists($file)) { 251 $cache->depends['files'][] = $file; 252 } 253 } 254 } 255 256 257 /** 258 * handle ajax requests 259 */ 260 public function ajaxCall(Event $event) 261 { 262 switch ($event->data) { 263 case 'plugin_doxycode_check_status': 264 case 'plugin_doxycode_get_snippet_html': 265 case 'plugin_doxycode_get_tag_files': 266 break; 267 default: 268 return; 269 } 270 271 // no other ajax call handlers needed 272 $event->stopPropagation(); 273 $event->preventDefault(); 274 275 if ($event->data === 'plugin_doxycode_check_status') { 276 // the main syntax component has put placeholders into the page while rendering 277 // the client tries to get the newest state for the doxygen builds executed through the buildmanager 278 279 /** @var helper_plugin_doxycode_buildmanager $buildmanager */ 280 $buildmanager = plugin_load('helper', 'doxycode_buildmanager'); 281 282 // the client sends us an array with the cache names (md5 hashes) 283 global $INPUT; 284 $hashes = $INPUT->arr('hashes'); 285 286 // get the job state for each XML file 287 foreach ($hashes as &$hash) { 288 if (strlen($hash['xmlHash']) > 0) { 289 $hash['state'] = $buildmanager->getJobState($hash['xmlHash']); 290 } 291 } 292 293 //set content type 294 header('Content-Type: application/json'); 295 echo json_encode($hashes); 296 297 return; 298 } // plugin_doxycode_check_status 299 300 if ($event->data === 'plugin_doxycode_get_snippet_html') { 301 header('Content-Type: application/json'); 302 303 // a client tries to dynamically load the rendered code snippet 304 global $INPUT; 305 $hashes = $INPUT->arr('hashes'); 306 307 $xml_cacheID = $hashes['xmlHash']; 308 $html_cacheID = $hashes['htmlHash']; 309 310 /** @var helper_plugin_doxycode $helper */ 311 $helper = plugin_load('helper', 'doxycode'); 312 /** @var helper_plugin_doxycode_buildmanager $buildmanager */ 313 $buildmanager = plugin_load('helper', 'doxycode_buildmanager'); 314 /** @var helper_plugin_doxycode_parser $parser */ 315 $parser = plugin_load('helper', 'doxycode_parser'); 316 /** @var helper_plugin_doxycode_tagmanager $tagmanager */ 317 $tagmanager = plugin_load('helper', 'doxycode_tagmanager'); 318 319 // maybe the snippet was already rendered in the meantime (by another ajax call or through page reload) 320 $html_cache = new Cache($html_cacheID, '.html'); 321 $xml_cache = new Cache($xml_cacheID, '.xml'); 322 323 $task_config = $buildmanager->getJobTaskConf($xml_cacheID); 324 $tag_conf = $tagmanager->getFilteredTagConfig($task_config['tagfiles']); 325 326 $depends = []; 327 $helper->getHTMLFileDependencies($depends, $xml_cacheID, $tag_conf); 328 329 $data = [ 330 'success' => false, 331 'hashes' => $hashes, 332 'html' => '' 333 ]; 334 335 // how will we generate the dependencies? 336 if ($html_cache->useCache($depends)) { 337 // we have a valid HTML rendered! 338 339 if ($cachedContent = @file_get_contents($html_cache->cache)) { 340 // add HTML cache to response 341 342 $data['html'] = $cachedContent; 343 $data['success'] = true; 344 345 echo json_encode($data); 346 return; 347 } 348 } 349 350 $job_config = $buildmanager->getJobConf($xml_cacheID); 351 352 $depends = []; 353 $helper->getXMLFileDependencies($depends, $tag_conf); 354 //set content type 355 356 if ($xml_cache->useCache($depends)) { 357 // we have a valid XML! 358 359 $xml_content = @file_get_contents($xml_cache->cache); 360 361 $rendered_text = $parser->renderXMLToDokuWikiCode($xml_content, $job_config['linenumbers'], $tag_conf); 362 363 // save content to cache 364 @file_put_contents($html_cache->cache, $rendered_text); 365 366 // add HTML to response 367 $data['html'] = $rendered_text; 368 $data['success'] = true; 369 370 echo json_encode($data); 371 372 return; 373 } 374 375 echo json_encode($data); 376 return; 377 } // plugin_doxycode_get_snippet_html 378 379 if ($event->data === 'plugin_doxycode_get_tag_files') { 380 // the client has requested a list of available tag file configurations 381 382 /** @var helper_plugin_doxycode_tagmanager $tagmanager */ 383 $tagmanager = plugin_load('helper', 'doxycode_tagmanager'); 384 385 // load the tag file configuration 386 $tag_config = $tagmanager->getFilteredTagConfig(); 387 388 header('Content-Type: application/json'); 389 390 // send data 391 echo json_encode($tag_config); 392 393 return; 394 } 395 } 396 397 public function insertTagButton(Event $event) 398 { 399 $event->data[] = array ( 400 'type' => 'doxycodeTagSelector', 401 'title' => 'doxycode', 402 'icon' => DOKU_REL . 'lib/plugins/doxycode/images/toolbar/doxycode.png', 403 'open' => '<doxycode>', 404 'close' => '</doxycode>', 405 'block' => false 406 ); 407 } 408 409 public function addRpcMethod(&$event, $param) 410 { 411 $my_rpc_call = array( 412 'doxycode.listTagFiles' => array('doxycode', 'listTagFiles'), 413 'doxycode.uploadTagFile' => array('doxycode', 'uploadTagFile') 414 ); 415 $event->data = array_merge($event->data, $my_rpc_call); 416 } 417} 418