1<?php 2/** 3 * Utilities for accessing the parser 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Harry Fuecks <hfuecks@gmail.com> 7 * @author Andreas Gohr <andi@splitbrain.org> 8 */ 9use dokuwiki\Extension\PluginInterface; 10use dokuwiki\Cache\CacheInstructions; 11use dokuwiki\Cache\CacheRenderer; 12use dokuwiki\ChangeLog\PageChangeLog; 13use dokuwiki\Extension\PluginController; 14use dokuwiki\Extension\Event; 15use dokuwiki\Extension\SyntaxPlugin; 16use dokuwiki\Parsing\Parser; 17use dokuwiki\Parsing\ParserMode\Acronym; 18use dokuwiki\Parsing\ParserMode\Camelcaselink; 19use dokuwiki\Parsing\ParserMode\Entity; 20use dokuwiki\Parsing\ParserMode\Formatting; 21use dokuwiki\Parsing\ParserMode\Smiley; 22 23/** 24 * How many pages shall be rendered for getting metadata during one request 25 * at maximum? Note that this limit isn't respected when METADATA_RENDER_UNLIMITED 26 * is passed as render parameter to p_get_metadata. 27 */ 28if (!defined('P_GET_METADATA_RENDER_LIMIT')) define('P_GET_METADATA_RENDER_LIMIT', 5); 29 30/** Don't render metadata even if it is outdated or doesn't exist */ 31define('METADATA_DONT_RENDER', 0); 32/** 33 * Render metadata when the page is really newer or the metadata doesn't exist. 34 * Uses just a simple check, but should work pretty well for loading simple 35 * metadata values like the page title and avoids rendering a lot of pages in 36 * one request. The P_GET_METADATA_RENDER_LIMIT is used in this mode. 37 * Use this if it is unlikely that the metadata value you are requesting 38 * does depend e.g. on pages that are included in the current page using 39 * the include plugin (this is very likely the case for the page title, but 40 * not for relation references). 41 */ 42define('METADATA_RENDER_USING_SIMPLE_CACHE', 1); 43/** 44 * Render metadata using the metadata cache logic. The P_GET_METADATA_RENDER_LIMIT 45 * is used in this mode. Use this mode when you are requesting more complex 46 * metadata. Although this will cause rendering more often it might actually have 47 * the effect that less current metadata is returned as it is more likely than in 48 * the simple cache mode that metadata needs to be rendered for all pages at once 49 * which means that when the metadata for the page is requested that actually needs 50 * to be updated the limit might have been reached already. 51 */ 52define('METADATA_RENDER_USING_CACHE', 2); 53/** 54 * Render metadata without limiting the number of pages for which metadata is 55 * rendered. Use this mode with care, normally it should only be used in places 56 * like the indexer or in cli scripts where the execution time normally isn't 57 * limited. This can be combined with the simple cache using 58 * METADATA_RENDER_USING_CACHE | METADATA_RENDER_UNLIMITED. 59 */ 60define('METADATA_RENDER_UNLIMITED', 4); 61 62/** 63 * Returns the parsed Wikitext in XHTML for the given id and revision. 64 * 65 * If $excuse is true an explanation is returned if the file 66 * wasn't found 67 * 68 * @param string $id page id 69 * @param string|int $rev revision timestamp or empty string 70 * @param bool $excuse 71 * @param string $date_at 72 * 73 * @return null|string 74 * @author Andreas Gohr <andi@splitbrain.org> 75 * 76 */ 77function p_wiki_xhtml($id, $rev = '', $excuse = true, $date_at = '') 78{ 79 $file = wikiFN($id, $rev); 80 $ret = ''; 81 82 //ensure $id is in global $ID (needed for parsing) 83 global $ID; 84 $keep = $ID; 85 $ID = $id; 86 87 if ($rev || $date_at) { 88 if (file_exists($file)) { 89 //no caching on old revisions 90 $ret = p_render('xhtml', p_get_instructions(io_readWikiPage($file, $id, $rev)), $info, $date_at); 91 } elseif ($excuse) { 92 $ret = p_locale_xhtml('norev'); 93 } 94 } elseif (file_exists($file)) { 95 $ret = p_cached_output($file, 'xhtml', $id); 96 } elseif ($excuse) { 97 //check if the page once existed 98 $changelog = new PageChangeLog($id); 99 if ($changelog->hasRevisions()) { 100 $ret = p_locale_xhtml('onceexisted'); 101 } else { 102 $ret = p_locale_xhtml('newpage'); 103 } 104 } 105 106 //restore ID (just in case) 107 $ID = $keep; 108 109 return $ret; 110} 111 112/** 113 * Returns the specified local text in parsed format 114 * 115 * @param string $id page id 116 * @return null|string 117 * @author Andreas Gohr <andi@splitbrain.org> 118 * 119 */ 120function p_locale_xhtml($id) 121{ 122 //fetch parsed locale 123 $data = ['id' => $id, 'html' => '']; 124 125 $event = new Event('PARSER_LOCALE_XHTML', $data); 126 if ($event->advise_before()) { 127 $data['html'] = p_cached_output(localeFN($data['id'])); 128 } 129 $event->advise_after(); 130 131 return $data['html']; 132} 133 134/** 135 * Returns the given file parsed into the requested output format 136 * 137 * @param string $file filename, path to file 138 * @param string $format 139 * @param string $id page id 140 * @return null|string 141 * @author Andreas Gohr <andi@splitbrain.org> 142 * @author Chris Smith <chris@jalakai.co.uk> 143 * 144 */ 145function p_cached_output($file, $format = 'xhtml', $id = '') 146{ 147 global $conf; 148 149 $cache = new CacheRenderer($id, $file, $format); 150 if ($cache->useCache()) { 151 $parsed = $cache->retrieveCache(false); 152 if ($conf['allowdebug'] && $format == 'xhtml') { 153 $parsed .= "\n<!-- cachefile {$cache->cache} used -->\n"; 154 } 155 } else { 156 $parsed = p_render($format, p_cached_instructions($file, false, $id), $info); 157 158 if (!empty($info['cache']) && $cache->storeCache($parsed)) { // storeCache() attempts to save cachefile 159 if ($conf['allowdebug'] && $format == 'xhtml') { 160 $parsed .= "\n<!-- no cachefile used, but created {$cache->cache} -->\n"; 161 } 162 } else { 163 $cache->removeCache(); //try to delete cachefile 164 if ($conf['allowdebug'] && $format == 'xhtml') { 165 $parsed .= "\n<!-- no cachefile used, caching forbidden -->\n"; 166 } 167 } 168 } 169 170 return $parsed; 171} 172 173/** 174 * Returns the render instructions for a file 175 * 176 * Uses and creates a serialized cache file 177 * 178 * @param string $file filename, path to file 179 * @param bool $cacheonly 180 * @param string $id page id 181 * @return array|null 182 * @author Andreas Gohr <andi@splitbrain.org> 183 * 184 */ 185function p_cached_instructions($file, $cacheonly = false, $id = '') 186{ 187 static $run = null; 188 if (is_null($run)) $run = []; 189 190 $cache = new CacheInstructions($id, $file); 191 192 if ($cacheonly || $cache->useCache() || (isset($run[$file]) && !defined('DOKU_UNITTEST'))) { 193 return $cache->retrieveCache(); 194 } elseif (file_exists($file)) { 195 // no cache - do some work 196 $ins = p_get_instructions(io_readWikiPage($file, $id)); 197 if ($cache->storeCache($ins)) { 198 $run[$file] = true; // we won't rebuild these instructions in the same run again 199 } else { 200 msg('Unable to save cache file. Hint: disk full; file permissions; safe_mode setting.', -1); 201 } 202 return $ins; 203 } 204 205 return null; 206} 207 208/** 209 * turns a page into a list of instructions 210 * 211 * @param string $text raw wiki syntax text 212 * @return array a list of instruction arrays 213 * @author Harry Fuecks <hfuecks@gmail.com> 214 * @author Andreas Gohr <andi@splitbrain.org> 215 * 216 */ 217function p_get_instructions($text) 218{ 219 220 $modes = p_get_parsermodes(); 221 222 // Create the parser and handler 223 $Parser = new Parser(new Doku_Handler()); 224 225 //add modes to parser 226 foreach ($modes as $mode) { 227 $Parser->addMode($mode['mode'], $mode['obj']); 228 } 229 230 // Do the parsing 231 Event::createAndTrigger('PARSER_WIKITEXT_PREPROCESS', $text); 232 return $Parser->parse($text); 233} 234 235/** 236 * returns the metadata of a page 237 * 238 * @param string $id The id of the page the metadata should be returned from 239 * @param string $key The key of the metdata value that shall be read (by default everything) 240 * separate hierarchies by " " like "date created" 241 * @param int $render If the page should be rendererd - possible values: 242 * METADATA_DONT_RENDER, METADATA_RENDER_USING_SIMPLE_CACHE, METADATA_RENDER_USING_CACHE 243 * METADATA_RENDER_UNLIMITED (also combined with the previous two options), 244 * default: METADATA_RENDER_USING_CACHE 245 * @return mixed The requested metadata fields 246 * 247 * @author Esther Brunner <esther@kaffeehaus.ch> 248 * @author Michael Hamann <michael@content-space.de> 249 */ 250function p_get_metadata($id, $key = '', $render = METADATA_RENDER_USING_CACHE) 251{ 252 global $ID; 253 static $render_count = 0; 254 // track pages that have already been rendered in order to avoid rendering the same page 255 // again 256 static $rendered_pages = []; 257 258 // cache the current page 259 // Benchmarking shows the current page's metadata is generally the only page metadata 260 // accessed several times. This may catch a few other pages, but that shouldn't be an issue. 261 $cache = ($ID == $id); 262 $meta = p_read_metadata($id, $cache); 263 264 if (!is_numeric($render)) { 265 if ($render) { 266 $render = METADATA_RENDER_USING_SIMPLE_CACHE; 267 } else { 268 $render = METADATA_DONT_RENDER; 269 } 270 } 271 272 // prevent recursive calls in the cache 273 static $recursion = false; 274 if (!$recursion && $render != METADATA_DONT_RENDER && !isset($rendered_pages[$id]) && page_exists($id)) { 275 $recursion = true; 276 277 $cachefile = new CacheRenderer($id, wikiFN($id), 'metadata'); 278 279 $do_render = false; 280 if ($render & METADATA_RENDER_UNLIMITED || $render_count < P_GET_METADATA_RENDER_LIMIT) { 281 if ($render & METADATA_RENDER_USING_SIMPLE_CACHE) { 282 $pagefn = wikiFN($id); 283 $metafn = metaFN($id, '.meta'); 284 if (!file_exists($metafn) || @filemtime($pagefn) > @filemtime($cachefile->cache)) { 285 $do_render = true; 286 } 287 } elseif (!$cachefile->useCache()) { 288 $do_render = true; 289 } 290 } 291 if ($do_render) { 292 if (!defined('DOKU_UNITTEST')) { 293 ++$render_count; 294 $rendered_pages[$id] = true; 295 } 296 $old_meta = $meta; 297 $meta = p_render_metadata($id, $meta); 298 // only update the file when the metadata has been changed 299 if ($meta == $old_meta || p_save_metadata($id, $meta)) { 300 // store a timestamp in order to make sure that the cachefile is touched 301 // this timestamp is also stored when the meta data is still the same 302 $cachefile->storeCache(time()); 303 } else { 304 msg('Unable to save metadata file. Hint: disk full; file permissions; safe_mode setting.', -1); 305 } 306 } 307 308 $recursion = false; 309 } 310 311 $val = $meta['current']; 312 313 // filter by $key 314 foreach (preg_split('/\s+/', $key, 2, PREG_SPLIT_NO_EMPTY) as $cur_key) { 315 if (!isset($val[$cur_key])) { 316 return null; 317 } 318 $val = $val[$cur_key]; 319 } 320 return $val; 321} 322 323/** 324 * sets metadata elements of a page 325 * 326 * @see http://www.dokuwiki.org/devel:metadata#functions_to_get_and_set_metadata 327 * 328 * @param string $id is the ID of a wiki page 329 * @param array $data is an array with key ⇒ value pairs to be set in the metadata 330 * @param boolean $render whether or not the page metadata should be generated with the renderer 331 * @param boolean $persistent indicates whether or not the particular metadata value will persist through 332 * the next metadata rendering. 333 * @return boolean true on success 334 * 335 * @author Esther Brunner <esther@kaffeehaus.ch> 336 * @author Michael Hamann <michael@content-space.de> 337 */ 338function p_set_metadata($id, $data, $render = false, $persistent = true) 339{ 340 if (!is_array($data)) return false; 341 342 global $ID, $METADATA_RENDERERS; 343 344 // if there is currently a renderer change the data in the renderer instead 345 if (isset($METADATA_RENDERERS[$id])) { 346 $orig =& $METADATA_RENDERERS[$id]; 347 $meta = $orig; 348 } else { 349 // cache the current page 350 $cache = ($ID == $id); 351 $orig = p_read_metadata($id, $cache); 352 353 // render metadata first? 354 $meta = $render ? p_render_metadata($id, $orig) : $orig; 355 } 356 357 // now add the passed metadata 358 $protected = ['description', 'date', 'contributor']; 359 foreach ($data as $key => $value) { 360 // be careful with sub-arrays of $meta['relation'] 361 if ($key == 'relation') { 362 foreach ($value as $subkey => $subvalue) { 363 if (isset($meta['current'][$key][$subkey]) && is_array($meta['current'][$key][$subkey])) { 364 $meta['current'][$key][$subkey] = array_replace($meta['current'][$key][$subkey], (array)$subvalue); 365 } else { 366 $meta['current'][$key][$subkey] = $subvalue; 367 } 368 if ($persistent) { 369 if (isset($meta['persistent'][$key][$subkey]) && is_array($meta['persistent'][$key][$subkey])) { 370 $meta['persistent'][$key][$subkey] = array_replace( 371 $meta['persistent'][$key][$subkey], 372 (array)$subvalue 373 ); 374 } else { 375 $meta['persistent'][$key][$subkey] = $subvalue; 376 } 377 } 378 } 379 380 // be careful with some senisitive arrays of $meta 381 } elseif (in_array($key, $protected)) { 382 // these keys, must have subkeys - a legitimate value must be an array 383 if (is_array($value)) { 384 $meta['current'][$key] = empty($meta['current'][$key]) ? 385 $value : 386 array_replace((array)$meta['current'][$key], $value); 387 388 if ($persistent) { 389 $meta['persistent'][$key] = empty($meta['persistent'][$key]) ? 390 $value : 391 array_replace((array)$meta['persistent'][$key], $value); 392 } 393 } 394 395 // no special treatment for the rest 396 } else { 397 $meta['current'][$key] = $value; 398 if ($persistent) $meta['persistent'][$key] = $value; 399 } 400 } 401 402 // save only if metadata changed 403 if ($meta == $orig) return true; 404 405 if (isset($METADATA_RENDERERS[$id])) { 406 // set both keys individually as the renderer has references to the individual keys 407 $METADATA_RENDERERS[$id]['current'] = $meta['current']; 408 $METADATA_RENDERERS[$id]['persistent'] = $meta['persistent']; 409 return true; 410 } else { 411 return p_save_metadata($id, $meta); 412 } 413} 414 415/** 416 * Purges the non-persistant part of the meta data 417 * used on page deletion 418 * 419 * @param string $id page id 420 * @return bool success / fail 421 * @author Michael Klier <chi@chimeric.de> 422 * 423 */ 424function p_purge_metadata($id) 425{ 426 $meta = p_read_metadata($id); 427 foreach ($meta['current'] as $key => $value) { 428 if (isset($meta[$key]) && is_array($meta[$key])) { 429 $meta['current'][$key] = []; 430 } else { 431 $meta['current'][$key] = ''; 432 } 433 } 434 return p_save_metadata($id, $meta); 435} 436 437/** 438 * read the metadata from source/cache for $id 439 * (internal use only - called by p_get_metadata & p_set_metadata) 440 * 441 * @param string $id absolute wiki page id 442 * @param bool $cache whether or not to cache metadata in memory 443 * (only use for metadata likely to be accessed several times) 444 * 445 * @return array metadata 446 * @author Christopher Smith <chris@jalakai.co.uk> 447 * 448 */ 449function p_read_metadata($id, $cache = false) 450{ 451 global $cache_metadata; 452 453 if (isset($cache_metadata[(string)$id])) return $cache_metadata[(string)$id]; 454 455 $file = metaFN($id, '.meta'); 456 $meta = file_exists($file) ? 457 unserialize(io_readFile($file, false)) : 458 ['current' => [], 'persistent' => []]; 459 460 if ($cache) { 461 $cache_metadata[(string)$id] = $meta; 462 } 463 464 return $meta; 465} 466 467/** 468 * This is the backend function to save a metadata array to a file 469 * 470 * @param string $id absolute wiki page id 471 * @param array $meta metadata 472 * 473 * @return bool success / fail 474 */ 475function p_save_metadata($id, $meta) 476{ 477 // sync cached copies, including $INFO metadata 478 global $cache_metadata, $INFO; 479 480 if (isset($cache_metadata[$id])) $cache_metadata[$id] = $meta; 481 if (!empty($INFO) && isset($INFO['id']) && ($id == $INFO['id'])) { 482 $INFO['meta'] = $meta['current']; 483 } 484 485 return io_saveFile(metaFN($id, '.meta'), serialize($meta)); 486} 487 488/** 489 * renders the metadata of a page 490 * 491 * @param string $id page id 492 * @param array $orig the original metadata 493 * @return array|null array('current'=> array,'persistent'=> array); 494 * @author Esther Brunner <esther@kaffeehaus.ch> 495 * 496 */ 497function p_render_metadata($id, $orig) 498{ 499 // make sure the correct ID is in global ID 500 global $ID, $METADATA_RENDERERS; 501 502 // avoid recursive rendering processes for the same id 503 if (isset($METADATA_RENDERERS[$id])) { 504 return $orig; 505 } 506 507 // store the original metadata in the global $METADATA_RENDERERS so p_set_metadata can use it 508 $METADATA_RENDERERS[$id] =& $orig; 509 510 $keep = $ID; 511 $ID = $id; 512 513 // add an extra key for the event - to tell event handlers the page whose metadata this is 514 $orig['page'] = $id; 515 $evt = new Event('PARSER_METADATA_RENDER', $orig); 516 if ($evt->advise_before()) { 517 // get instructions 518 $instructions = p_cached_instructions(wikiFN($id), false, $id); 519 if (is_null($instructions)) { 520 $ID = $keep; 521 unset($METADATA_RENDERERS[$id]); 522 return null; // something went wrong with the instructions 523 } 524 525 // set up the renderer 526 $renderer = new Doku_Renderer_metadata(); 527 $renderer->meta =& $orig['current']; 528 $renderer->persistent =& $orig['persistent']; 529 530 // loop through the instructions 531 foreach ($instructions as $instruction) { 532 // execute the callback against the renderer 533 call_user_func_array([&$renderer, $instruction[0]], (array)$instruction[1]); 534 } 535 536 $evt->result = ['current' => &$renderer->meta, 'persistent' => &$renderer->persistent]; 537 } 538 $evt->advise_after(); 539 540 // clean up 541 $ID = $keep; 542 unset($METADATA_RENDERERS[$id]); 543 return $evt->result; 544} 545 546/** 547 * returns all available parser syntax modes in correct order 548 * 549 * @return array[] with for each plugin the array('sort' => sortnumber, 'mode' => mode string, 'obj' => plugin object) 550 * @author Andreas Gohr <andi@splitbrain.org> 551 * 552 */ 553function p_get_parsermodes() 554{ 555 global $conf; 556 557 //reuse old data 558 static $modes = null; 559 if ($modes != null && !defined('DOKU_UNITTEST')) { 560 return $modes; 561 } 562 563 //import parser classes and mode definitions 564 require_once DOKU_INC . 'inc/parser/parser.php'; 565 566 // we now collect all syntax modes and their objects, then they will 567 // be sorted and added to the parser in correct order 568 $modes = []; 569 570 // add syntax plugins 571 $pluginlist = plugin_list('syntax'); 572 if ($pluginlist !== []) { 573 global $PARSER_MODES; 574 foreach ($pluginlist as $p) { 575 /** @var SyntaxPlugin $obj */ 576 if (!($obj = plugin_load('syntax', $p)) instanceof PluginInterface) continue; 577 $PARSER_MODES[$obj->getType()][] = "plugin_$p"; //register mode type 578 //add to modes 579 $modes[] = [ 580 'sort' => $obj->getSort(), 581 'mode' => "plugin_$p", 582 'obj' => $obj, 583 ]; 584 unset($obj); //remove the reference 585 } 586 } 587 588 // add default modes 589 $std_modes = [ 590 'listblock', 'preformatted', 'notoc', 'nocache', 'header', 'table', 'linebreak', 'footnote', 'hr', 591 'unformatted', 'code', 'file', 'quote', 'internallink', 'rss', 'media', 'externallink', 592 'emaillink', 'windowssharelink', 'eol' 593 ]; 594 if ($conf['typography']) { 595 $std_modes[] = 'quotes'; 596 $std_modes[] = 'multiplyentity'; 597 } 598 foreach ($std_modes as $m) { 599 $class = 'dokuwiki\\Parsing\\ParserMode\\' . ucfirst($m); 600 $obj = new $class(); 601 $modes[] = ['sort' => $obj->getSort(), 'mode' => $m, 'obj' => $obj]; 602 } 603 604 // add formatting modes 605 $fmt_modes = [ 606 'strong', 'emphasis', 'underline', 'monospace', 'subscript', 'superscript', 'deleted' 607 ]; 608 foreach ($fmt_modes as $m) { 609 $obj = new Formatting($m); 610 $modes[] = [ 611 'sort' => $obj->getSort(), 612 'mode' => $m, 613 'obj' => $obj 614 ]; 615 } 616 617 // add modes which need files 618 $obj = new Smiley(array_keys(getSmileys())); 619 $modes[] = ['sort' => $obj->getSort(), 'mode' => 'smiley', 'obj' => $obj]; 620 $obj = new Acronym(array_keys(getAcronyms())); 621 $modes[] = ['sort' => $obj->getSort(), 'mode' => 'acronym', 'obj' => $obj]; 622 $obj = new Entity(array_keys(getEntities())); 623 $modes[] = ['sort' => $obj->getSort(), 'mode' => 'entity', 'obj' => $obj]; 624 625 // add optional camelcase mode 626 if ($conf['camelcase']) { 627 $obj = new Camelcaselink(); 628 $modes[] = ['sort' => $obj->getSort(), 'mode' => 'camelcaselink', 'obj' => $obj]; 629 } 630 631 //sort modes 632 usort($modes, 'p_sort_modes'); 633 634 return $modes; 635} 636 637/** 638 * Callback function for usort 639 * 640 * @param array $a 641 * @param array $b 642 * @return int $a is lower/equal/higher than $b 643 * @author Andreas Gohr <andi@splitbrain.org> 644 * 645 */ 646function p_sort_modes($a, $b) 647{ 648 return $a['sort'] <=> $b['sort']; 649} 650 651/** 652 * Renders a list of instruction to the specified output mode 653 * 654 * In the $info array is information from the renderer returned 655 * 656 * @param string $mode 657 * @param array|null|false $instructions 658 * @param array $info returns render info like enabled toc and cache 659 * @param string $date_at 660 * @return null|string rendered output 661 * @author Andreas Gohr <andi@splitbrain.org> 662 * 663 * @author Harry Fuecks <hfuecks@gmail.com> 664 */ 665function p_render($mode, $instructions, &$info, $date_at = '') 666{ 667 if (is_null($instructions)) return ''; 668 if ($instructions === false) return ''; 669 670 $Renderer = p_get_renderer($mode); 671 if (is_null($Renderer)) return null; 672 673 $Renderer->reset(); 674 675 if ($date_at) { 676 $Renderer->date_at = $date_at; 677 } 678 679 $Renderer->smileys = getSmileys(); 680 $Renderer->entities = getEntities(); 681 $Renderer->acronyms = getAcronyms(); 682 $Renderer->interwiki = getInterwiki(); 683 684 // Loop through the instructions 685 foreach ($instructions as $instruction) { 686 // Execute the callback against the Renderer 687 if (method_exists($Renderer, $instruction[0])) { 688 call_user_func_array([&$Renderer, $instruction[0]], $instruction[1] ?: []); 689 } 690 } 691 692 //set info array 693 $info = $Renderer->info; 694 695 // Post process and return the output 696 $data = [$mode, & $Renderer->doc]; 697 Event::createAndTrigger('RENDERER_CONTENT_POSTPROCESS', $data); 698 return $Renderer->doc; 699} 700 701/** 702 * Figure out the correct renderer class to use for $mode, 703 * instantiate and return it 704 * 705 * @param string $mode Mode of the renderer to get 706 * @return null|Doku_Renderer The renderer 707 * 708 * @author Christopher Smith <chris@jalakai.co.uk> 709 */ 710function p_get_renderer($mode) 711{ 712 /** @var PluginController $plugin_controller */ 713 global $conf, $plugin_controller; 714 715 $rname = empty($conf['renderer_' . $mode]) ? $mode : $conf['renderer_' . $mode]; 716 $rclass = "Doku_Renderer_$rname"; 717 718 // if requested earlier or a bundled renderer 719 if (class_exists($rclass)) { 720 return new $rclass(); 721 } 722 723 // not bundled, see if its an enabled renderer plugin & when $mode is 'xhtml', the renderer can supply that format. 724 /** @var Doku_Renderer $Renderer */ 725 $Renderer = $plugin_controller->load('renderer', $rname); 726 if ($Renderer && is_a($Renderer, 'Doku_Renderer') && ($mode != 'xhtml' || $mode == $Renderer->getFormat())) { 727 return $Renderer; 728 } 729 730 // there is a configuration error! 731 // not bundled, not a valid enabled plugin, use $mode to try to fallback to a bundled renderer 732 $rclass = "Doku_Renderer_$mode"; 733 if (class_exists($rclass)) { 734 // viewers should see renderered output, so restrict the warning to admins only 735 $msg = "No renderer '$rname' found for mode '$mode', check your plugins"; 736 if ($mode == 'xhtml') { 737 $msg .= " and the 'renderer_xhtml' config setting"; 738 } 739 $msg .= ".<br/>Attempting to fallback to the bundled renderer."; 740 msg($msg, -1, '', '', MSG_ADMINS_ONLY); 741 742 $Renderer = new $rclass(); 743 $Renderer->nocache(); // fallback only (and may include admin alerts), don't cache 744 return $Renderer; 745 } 746 747 // fallback failed, alert the world 748 msg("No renderer '$rname' found for mode '$mode'", -1); 749 return null; 750} 751 752/** 753 * Gets the first heading from a file 754 * 755 * @param string $id dokuwiki page id 756 * @param int $render rerender if first heading not known 757 * default: METADATA_RENDER_USING_SIMPLE_CACHE 758 * Possible values: METADATA_DONT_RENDER, 759 * METADATA_RENDER_USING_SIMPLE_CACHE, 760 * METADATA_RENDER_USING_CACHE, 761 * METADATA_RENDER_UNLIMITED 762 * @return string|null The first heading 763 * 764 * @author Andreas Gohr <andi@splitbrain.org> 765 * @author Michael Hamann <michael@content-space.de> 766 */ 767function p_get_first_heading($id, $render = METADATA_RENDER_USING_SIMPLE_CACHE) 768{ 769 return p_get_metadata(cleanID($id), 'title', $render); 770} 771 772/** 773 * Wrapper for GeSHi Code Highlighter, provides caching of its output 774 * 775 * @param string $code source code to be highlighted 776 * @param string $language language to provide highlighting 777 * @param string $wrapper html element to wrap the returned highlighted text 778 * @return string xhtml code 779 * 780 * @author Christopher Smith <chris@jalakai.co.uk> 781 * @author Andreas Gohr <andi@splitbrain.org> 782 */ 783function p_xhtml_cached_geshi($code, $language, $wrapper = 'pre', array $options = null) 784{ 785 global $conf, $config_cascade, $INPUT; 786 $language = strtolower($language); 787 788 // remove any leading or trailing blank lines 789 $code = preg_replace('/^\s*?\n|\s*?\n$/', '', $code); 790 791 $optionsmd5 = md5(serialize($options)); 792 $cache = getCacheName($language . $code . $optionsmd5, ".code"); 793 $ctime = @filemtime($cache); 794 if ( 795 $ctime && !$INPUT->bool('purge') && 796 $ctime > filemtime(DOKU_INC . 'vendor/composer/installed.json') && // libraries changed 797 $ctime > filemtime(reset($config_cascade['main']['default'])) 798 ) { // dokuwiki changed 799 $highlighted_code = io_readFile($cache, false); 800 } else { 801 $geshi = new GeSHi($code, $language); 802 $geshi->set_encoding('utf-8'); 803 $geshi->enable_classes(); 804 $geshi->set_header_type(GESHI_HEADER_PRE); 805 $geshi->set_link_target($conf['target']['extern']); 806 if ($options !== null) { 807 foreach ($options as $function => $params) { 808 if (is_callable([$geshi, $function])) { 809 $geshi->$function($params); 810 } 811 } 812 } 813 814 // remove GeSHi's wrapper element (we'll replace it with our own later) 815 // we need to use a GeSHi wrapper to avoid <BR> throughout the highlighted text 816 $highlighted_code = trim(preg_replace('!^<pre[^>]*>|</pre>$!', '', $geshi->parse_code()), "\n\r"); 817 io_saveFile($cache, $highlighted_code); 818 } 819 820 // add a wrapper element if required 821 if ($wrapper) { 822 return "<$wrapper class=\"code $language\">$highlighted_code</$wrapper>"; 823 } else { 824 return $highlighted_code; 825 } 826} 827