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