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