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