xref: /dokuwiki/inc/template.php (revision dd9e8e5ea54469964faab99223a61bd48146ac42)
1<?php
2
3/**
4 * DokuWiki template functions
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Andreas Gohr <andi@splitbrain.org>
8 */
9
10use dokuwiki\ActionRouter;
11use dokuwiki\Action\Exception\FatalException;
12use dokuwiki\Extension\PluginInterface;
13use dokuwiki\File\MediaFile;
14use dokuwiki\Ui\Admin;
15use dokuwiki\StyleUtils;
16use dokuwiki\Menu\Item\AbstractItem;
17use dokuwiki\Form\Form;
18use dokuwiki\Menu\MobileMenu;
19use dokuwiki\Ui\Subscribe;
20use dokuwiki\Extension\AdminPlugin;
21use dokuwiki\Extension\Event;
22use dokuwiki\File\PageResolver;
23
24/**
25 * Access a template file
26 *
27 * Returns the path to the given file inside the current template, uses
28 * default template if the custom version doesn't exist.
29 *
30 * @param string $file
31 * @return string
32 *
33 * @author Andreas Gohr <andi@splitbrain.org>
34 */
35function template($file)
36{
37    global $conf;
38
39    if (@is_readable(DOKU_INC . 'lib/tpl/' . $conf['template'] . '/' . $file))
40        return DOKU_INC . 'lib/tpl/' . $conf['template'] . '/' . $file;
41
42    return DOKU_INC . 'lib/tpl/dokuwiki/' . $file;
43}
44
45/**
46 * Convenience function to access template dir from local FS
47 *
48 * This replaces the deprecated DOKU_TPLINC constant
49 *
50 * @param string $tpl The template to use, default to current one
51 * @return string
52 *
53 * @author Andreas Gohr <andi@splitbrain.org>
54 */
55function tpl_incdir($tpl = '')
56{
57    global $conf;
58    if (!$tpl) $tpl = $conf['template'];
59    return DOKU_INC . 'lib/tpl/' . $tpl . '/';
60}
61
62/**
63 * Convenience function to access template dir from web
64 *
65 * This replaces the deprecated DOKU_TPL constant
66 *
67 * @param string $tpl The template to use, default to current one
68 * @return string
69 *
70 * @author Andreas Gohr <andi@splitbrain.org>
71 */
72function tpl_basedir($tpl = '')
73{
74    global $conf;
75    if (!$tpl) $tpl = $conf['template'];
76    return DOKU_BASE . 'lib/tpl/' . $tpl . '/';
77}
78
79/**
80 * Print the content
81 *
82 * This function is used for printing all the usual content
83 * (defined by the global $ACT var) by calling the appropriate
84 * outputfunction(s) from html.php
85 *
86 * Everything that doesn't use the main template file isn't
87 * handled by this function. ACL stuff is not done here either.
88 *
89 * @param bool $prependTOC should the TOC be displayed here?
90 * @return bool true if any output
91 *
92 * @triggers TPL_ACT_RENDER
93 * @triggers TPL_CONTENT_DISPLAY
94 * @author Andreas Gohr <andi@splitbrain.org>
95 */
96function tpl_content($prependTOC = true)
97{
98    global $ACT;
99    global $INFO;
100    $INFO['prependTOC'] = $prependTOC;
101
102    ob_start();
103    Event::createAndTrigger('TPL_ACT_RENDER', $ACT, 'tpl_content_core');
104    $html_output = ob_get_clean();
105    Event::createAndTrigger('TPL_CONTENT_DISPLAY', $html_output, function ($html_output) {
106        echo $html_output;
107    });
108
109    return !empty($html_output);
110}
111
112/**
113 * Default Action of TPL_ACT_RENDER
114 *
115 * @return bool
116 */
117function tpl_content_core()
118{
119    $router = ActionRouter::getInstance();
120    try {
121        $router->getAction()->tplContent();
122    } catch (FatalException $e) {
123        // there was no content for the action
124        msg(hsc($e->getMessage()), -1);
125        return false;
126    }
127    return true;
128}
129
130/**
131 * Places the TOC where the function is called
132 *
133 * If you use this you most probably want to call tpl_content with
134 * a false argument
135 *
136 * @param bool $return Should the TOC be returned instead to be printed?
137 * @return string
138 *
139 * @author Andreas Gohr <andi@splitbrain.org>
140 */
141function tpl_toc($return = false)
142{
143    global $TOC;
144    global $ACT;
145    global $ID;
146    global $REV;
147    global $INFO;
148    global $conf;
149    $toc = [];
150
151    if (is_array($TOC)) {
152        // if a TOC was prepared in global scope, always use it
153        $toc = $TOC;
154    } elseif (($ACT == 'show' || str_starts_with($ACT, 'export')) && !$REV && $INFO['exists']) {
155        // get TOC from metadata, render if neccessary
156        $meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
157        $tocok = $meta['internal']['toc'] ?? true;
158        $toc = $meta['description']['tableofcontents'] ?? null;
159        if (!$tocok || !is_array($toc) || !$conf['tocminheads'] || count($toc) < $conf['tocminheads']) {
160            $toc = [];
161        }
162    } elseif ($ACT == 'admin') {
163        // try to load admin plugin TOC
164        /** @var AdminPlugin $plugin */
165        if ($plugin = plugin_getRequestAdminPlugin()) {
166            $toc = $plugin->getTOC();
167            $TOC = $toc; // avoid later rebuild
168        }
169    }
170
171    Event::createAndTrigger('TPL_TOC_RENDER', $toc, null, false);
172    $html = html_TOC($toc);
173    if ($return) return $html;
174    echo $html;
175    return '';
176}
177
178/**
179 * Handle the admin page contents
180 *
181 * @return bool
182 *
183 * @author Andreas Gohr <andi@splitbrain.org>
184 */
185function tpl_admin()
186{
187    global $INFO;
188    global $TOC;
189    global $INPUT;
190
191    $plugin = null;
192    $class = $INPUT->str('page');
193    if (!empty($class)) {
194        $pluginlist = plugin_list('admin');
195
196        if (in_array($class, $pluginlist)) {
197            // attempt to load the plugin
198            /** @var AdminPlugin $plugin */
199            $plugin = plugin_load('admin', $class);
200        }
201    }
202
203    if ($plugin instanceof PluginInterface) {
204        if (!is_array($TOC)) $TOC = $plugin->getTOC(); //if TOC wasn't requested yet
205        if ($INFO['prependTOC']) tpl_toc();
206        $plugin->html();
207    } else {
208        $admin = new Admin();
209        $admin->show();
210    }
211    return true;
212}
213
214/**
215 * Print the correct HTML meta headers
216 *
217 * This has to go into the head section of your template.
218 *
219 * @param bool $alt Should feeds and alternative format links be added?
220 * @return bool
221 * @throws JsonException
222 *
223 * @author Andreas Gohr <andi@splitbrain.org>
224 * @triggers TPL_METAHEADER_OUTPUT
225 */
226function tpl_metaheaders($alt = true)
227{
228    global $ID;
229    global $REV;
230    global $INFO;
231    global $JSINFO;
232    global $ACT;
233    global $QUERY;
234    global $lang;
235    global $conf;
236    global $updateVersion;
237    /** @var Input $INPUT */
238    global $INPUT;
239
240    // prepare the head array
241    $head = [];
242
243    // prepare seed for js and css
244    $tseed = $updateVersion;
245    $depends = getConfigFiles('main');
246    $depends[] = DOKU_CONF . "tpl/" . $conf['template'] . "/style.ini";
247    foreach ($depends as $f) $tseed .= @filemtime($f);
248    $tseed = md5($tseed);
249
250    // the usual stuff
251    $head['meta'][] = ['name' => 'generator', 'content' => 'DokuWiki'];
252    if (actionOK('search')) {
253        $head['link'][] = [
254            'rel' => 'search',
255            'type' => 'application/opensearchdescription+xml',
256            'href' => DOKU_BASE . 'lib/exe/opensearch.php',
257            'title' => $conf['title']
258        ];
259    }
260
261    $head['link'][] = ['rel' => 'start', 'href' => DOKU_BASE];
262    if (actionOK('index')) {
263        $head['link'][] = [
264            'rel' => 'contents',
265            'href' => wl($ID, 'do=index', false, '&'),
266            'title' => $lang['btn_index']
267        ];
268    }
269
270    if (actionOK('manifest')) {
271        $head['link'][] = [
272            'rel' => 'manifest',
273            'href' => DOKU_BASE . 'lib/exe/manifest.php',
274            'crossorigin' => 'use-credentials' // See issue #4322
275        ];
276    }
277
278    $styleUtil = new StyleUtils();
279    $styleIni = $styleUtil->cssStyleini();
280    $replacements = $styleIni['replacements'];
281    if (!empty($replacements['__theme_color__'])) {
282        $head['meta'][] = [
283            'name' => 'theme-color',
284            'content' => $replacements['__theme_color__']
285        ];
286    }
287
288    if ($alt) {
289        if (actionOK('rss')) {
290            $head['link'][] = [
291                'rel' => 'alternate',
292                'type' => 'application/rss+xml',
293                'title' => $lang['btn_recent'],
294                'href' => DOKU_BASE . 'feed.php'
295            ];
296            $head['link'][] = [
297                'rel' => 'alternate',
298                'type' => 'application/rss+xml',
299                'title' => $lang['currentns'],
300                'href' => DOKU_BASE . 'feed.php?mode=list&ns=' . (isset($INFO) ? $INFO['namespace'] : '')
301            ];
302        }
303        if (($ACT == 'show' || $ACT == 'search') && $INFO['writable']) {
304            $head['link'][] = [
305                'rel' => 'edit',
306                'title' => $lang['btn_edit'],
307                'href' => wl($ID, 'do=edit', false, '&')
308            ];
309        }
310
311        if (actionOK('rss') && $ACT == 'search') {
312            $head['link'][] = [
313                'rel' => 'alternate',
314                'type' => 'application/rss+xml',
315                'title' => $lang['searchresult'],
316                'href' => DOKU_BASE . 'feed.php?mode=search&q=' . $QUERY
317            ];
318        }
319
320        if (actionOK('export_xhtml')) {
321            $head['link'][] = [
322                'rel' => 'alternate',
323                'type' => 'text/html',
324                'title' => $lang['plainhtml'],
325                'href' => exportlink($ID, 'xhtml', '', false, '&')
326            ];
327        }
328
329        if (actionOK('export_raw')) {
330            $head['link'][] = [
331                'rel' => 'alternate',
332                'type' => 'text/plain',
333                'title' => $lang['wikimarkup'],
334                'href' => exportlink($ID, 'raw', '', false, '&')
335            ];
336        }
337    }
338
339    // setup robot tags appropriate for different modes
340    if (($ACT == 'show' || $ACT == 'export_xhtml') && !$REV) {
341        if ($INFO['exists']) {
342            //delay indexing:
343            if ((time() - $INFO['lastmod']) >= $conf['indexdelay'] && !isHiddenPage($ID)) {
344                $head['meta'][] = ['name' => 'robots', 'content' => 'index,follow'];
345            } else {
346                $head['meta'][] = ['name' => 'robots', 'content' => 'noindex,nofollow'];
347            }
348            $canonicalUrl = wl($ID, '', true, '&');
349            if ($ID == $conf['start']) {
350                $canonicalUrl = DOKU_URL;
351            }
352            $head['link'][] = ['rel' => 'canonical', 'href' => $canonicalUrl];
353        } else {
354            $head['meta'][] = ['name' => 'robots', 'content' => 'noindex,follow'];
355        }
356    } elseif (defined('DOKU_MEDIADETAIL')) {
357        $head['meta'][] = ['name' => 'robots', 'content' => 'index,follow'];
358    } else {
359        $head['meta'][] = ['name' => 'robots', 'content' => 'noindex,nofollow'];
360    }
361
362    // set metadata
363    if ($ACT == 'show' || $ACT == 'export_xhtml') {
364        // keywords (explicit or implicit)
365        if (!empty($INFO['meta']['subject'])) {
366            $head['meta'][] = ['name' => 'keywords', 'content' => implode(',', $INFO['meta']['subject'])];
367        } else {
368            $head['meta'][] = ['name' => 'keywords', 'content' => str_replace(':', ',', $ID)];
369        }
370    }
371
372    // load stylesheets
373    $head['link'][] = [
374        'rel' => 'stylesheet',
375        'href' => DOKU_BASE . 'lib/exe/css.php?t=' . rawurlencode($conf['template']) . '&tseed=' . $tseed
376    ];
377
378    $script = "var NS='" . (isset($INFO) ? $INFO['namespace'] : '') . "';";
379    if ($conf['useacl'] && $INPUT->server->str('REMOTE_USER')) {
380        $script .= "var SIG=" . toolbar_signature() . ";";
381    }
382    jsinfo();
383    $script .= 'var JSINFO = ' . json_encode($JSINFO, JSON_THROW_ON_ERROR) . ';';
384    $script .= '(function(H){H.className=H.className.replace(/\bno-js\b/,\'js\')})(document.documentElement);';
385    $head['script'][] = ['_data' => $script];
386
387    // load jquery
388    $jquery = getCdnUrls();
389    foreach ($jquery as $src) {
390        $head['script'][] = [
391                '_data' => '',
392                'src' => $src
393            ] + ($conf['defer_js'] ? ['defer' => 'defer'] : []);
394    }
395
396    // load our javascript dispatcher
397    $head['script'][] = [
398            '_data' => '',
399            'src' => DOKU_BASE . 'lib/exe/js.php' . '?t=' . rawurlencode($conf['template']) . '&tseed=' . $tseed
400        ] + ($conf['defer_js'] ? ['defer' => 'defer'] : []);
401
402    // trigger event here
403    Event::createAndTrigger('TPL_METAHEADER_OUTPUT', $head, '_tpl_metaheaders_action', true);
404    return true;
405}
406
407/**
408 * prints the array build by tpl_metaheaders
409 *
410 * $data is an array of different header tags. Each tag can have multiple
411 * instances. Attributes are given as key value pairs. Values will be HTML
412 * encoded automatically so they should be provided as is in the $data array.
413 *
414 * For tags having a body attribute specify the body data in the special
415 * attribute '_data'. This field will NOT BE ESCAPED automatically.
416 *
417 * Inline scripts will use any nonce provided in the environment variable 'NONCE'.
418 *
419 * @param array $data
420 *
421 * @author Andreas Gohr <andi@splitbrain.org>
422 */
423function _tpl_metaheaders_action($data)
424{
425    $nonce = getenv('NONCE');
426    foreach ($data as $tag => $inst) {
427        foreach ($inst as $attr) {
428            if (empty($attr)) {
429                continue;
430            }
431            if ($nonce && $tag == 'script' && !empty($attr['_data'])) {
432                $attr['nonce'] = $nonce; // add nonce to inline script tags
433            }
434            echo '<', $tag, ' ', buildAttributes($attr);
435            if (isset($attr['_data']) || $tag == 'script') {
436                echo '>', $attr['_data'] ?? '', '</', $tag, '>';
437            } else {
438                echo '/>';
439            }
440            echo "\n";
441        }
442    }
443}
444
445/**
446 * Output the given script as inline script tag
447 *
448 * This function will add the nonce attribute if a nonce is available.
449 *
450 * The script is NOT automatically escaped!
451 *
452 * @param string $script
453 * @param bool $return Return or print directly?
454 * @return string|void
455 */
456function tpl_inlineScript($script, $return = false)
457{
458    $nonce = getenv('NONCE');
459    if ($nonce) {
460        $script = '<script nonce="' . $nonce . '">' . $script . '</script>';
461    } else {
462        $script = '<script>' . $script . '</script>';
463    }
464
465    if ($return) return $script;
466    echo $script;
467}
468
469/**
470 * Print a link
471 *
472 * Just builds a link.
473 *
474 * @param string $url
475 * @param string $name
476 * @param string $more
477 * @param bool $return if true return the link html, otherwise print
478 * @return bool|string html of the link, or true if printed
479 *
480 * @author Andreas Gohr <andi@splitbrain.org>
481 */
482function tpl_link($url, $name, $more = '', $return = false)
483{
484    $out = '<a href="' . $url . '" ';
485    if ($more) $out .= ' ' . $more;
486    $out .= ">$name</a>";
487    if ($return) return $out;
488    echo $out;
489    return true;
490}
491
492/**
493 * Prints a link to a WikiPage
494 *
495 * Wrapper around html_wikilink
496 *
497 * @param string $id page id
498 * @param string|null $name the name of the link
499 * @param bool $return
500 * @return true|string
501 *
502 * @author Andreas Gohr <andi@splitbrain.org>
503 */
504function tpl_pagelink($id, $name = null, $return = false)
505{
506    $out = '<bdi>' . html_wikilink($id, $name) . '</bdi>';
507    if ($return) return $out;
508    echo $out;
509    return true;
510}
511
512/**
513 * get the parent page
514 *
515 * Tries to find out which page is parent.
516 * returns false if none is available
517 *
518 * @param string $id page id
519 * @return false|string
520 *
521 * @author Andreas Gohr <andi@splitbrain.org>
522 */
523function tpl_getparent($id)
524{
525    $resolver = new PageResolver('root');
526
527    $parent = getNS($id) . ':';
528    $parent = $resolver->resolveId($parent);
529    if ($parent == $id) {
530        $pos = strrpos(getNS($id), ':');
531        $parent = substr($parent, 0, $pos) . ':';
532        $parent = $resolver->resolveId($parent);
533        if ($parent == $id) return false;
534    }
535    return $parent;
536}
537
538/**
539 * Print one of the buttons
540 *
541 * @param string $type
542 * @param bool $return
543 * @return bool|string html, or false if no data, true if printed
544 * @see    tpl_get_action
545 *
546 * @author Adrian Lang <mail@adrianlang.de>
547 * @deprecated 2017-09-01 see devel:menus
548 */
549function tpl_button($type, $return = false)
550{
551    dbg_deprecated('see devel:menus');
552    $data = tpl_get_action($type);
553    if ($data === false) {
554        return false;
555    } elseif (!is_array($data)) {
556        $out = sprintf($data, 'button');
557    } else {
558        /**
559         * @var string $accesskey
560         * @var string $id
561         * @var string $method
562         * @var array $params
563         */
564        extract($data);
565        if ($id === '#dokuwiki__top') {
566            $out = html_topbtn();
567        } else {
568            $out = html_btn($type, $id, $accesskey, $params, $method);
569        }
570    }
571    if ($return) return $out;
572    echo $out;
573    return true;
574}
575
576/**
577 * Like the action buttons but links
578 *
579 * @param string $type action command
580 * @param string $pre prefix of link
581 * @param string $suf suffix of link
582 * @param string $inner innerHML of link
583 * @param bool $return if true it returns html, otherwise prints
584 * @return bool|string html or false if no data, true if printed
585 *
586 * @see    tpl_get_action
587 * @author Adrian Lang <mail@adrianlang.de>
588 * @deprecated 2017-09-01 see devel:menus
589 */
590function tpl_actionlink($type, $pre = '', $suf = '', $inner = '', $return = false)
591{
592    dbg_deprecated('see devel:menus');
593    global $lang;
594    $data = tpl_get_action($type);
595    if ($data === false) {
596        return false;
597    } elseif (!is_array($data)) {
598        $out = sprintf($data, 'link');
599    } else {
600        /**
601         * @var string $accesskey
602         * @var string $id
603         * @var string $method
604         * @var bool $nofollow
605         * @var array $params
606         * @var string $replacement
607         */
608        extract($data);
609        if (str_starts_with($id, '#')) {
610            $linktarget = $id;
611        } else {
612            $linktarget = wl($id, $params);
613        }
614        $caption = $lang['btn_' . $type];
615        if (strpos($caption, '%s')) {
616            $caption = sprintf($caption, $replacement);
617        }
618        $akey = '';
619        $addTitle = '';
620        if ($accesskey) {
621            $akey = 'accesskey="' . $accesskey . '" ';
622            $addTitle = ' [' . strtoupper($accesskey) . ']';
623        }
624        $rel = $nofollow ? 'rel="nofollow" ' : '';
625        $out = tpl_link(
626            $linktarget,
627            $pre . ($inner ?: $caption) . $suf,
628            'class="action ' . $type . '" ' .
629            $akey . $rel .
630            'title="' . hsc($caption) . $addTitle . '"',
631            true
632        );
633    }
634    if ($return) return $out;
635    echo $out;
636    return true;
637}
638
639/**
640 * Check the actions and get data for buttons and links
641 *
642 * @param string $type
643 * @return array|bool|string
644 *
645 * @author Adrian Lang <mail@adrianlang.de>
646 * @author Andreas Gohr <andi@splitbrain.org>
647 * @author Matthias Grimm <matthiasgrimm@users.sourceforge.net>
648 * @deprecated 2017-09-01 see devel:menus
649 */
650function tpl_get_action($type)
651{
652    dbg_deprecated('see devel:menus');
653    if ($type == 'history') $type = 'revisions';
654    if ($type == 'subscription') $type = 'subscribe';
655    if ($type == 'img_backto') $type = 'imgBackto';
656
657    $class = '\\dokuwiki\\Menu\\Item\\' . ucfirst($type);
658    if (class_exists($class)) {
659        try {
660            /** @var AbstractItem $item */
661            $item = new $class();
662            $data = $item->getLegacyData();
663            $unknown = false;
664        } catch (RuntimeException) {
665            return false;
666        }
667    } else {
668        global $ID;
669        $data = [
670            'accesskey' => null,
671            'type' => $type,
672            'id' => $ID,
673            'method' => 'get',
674            'params' => ['do' => $type],
675            'nofollow' => true,
676            'replacement' => ''
677        ];
678        $unknown = true;
679    }
680
681    $evt = new Event('TPL_ACTION_GET', $data);
682    if ($evt->advise_before()) {
683        //handle unknown types
684        if ($unknown) {
685            $data = '[unknown %s type]';
686        }
687    }
688    $evt->advise_after();
689    unset($evt);
690
691    return $data;
692}
693
694/**
695 * Wrapper around tpl_button() and tpl_actionlink()
696 *
697 * @param string $type action command
698 * @param bool $link link or form button?
699 * @param string|bool $wrapper HTML element wrapper
700 * @param bool $return return or print
701 * @param string $pre prefix for links
702 * @param string $suf suffix for links
703 * @param string $inner inner HTML for links
704 * @return bool|string
705 *
706 * @author Anika Henke <anika@selfthinker.org>
707 * @deprecated 2017-09-01 see devel:menus
708 */
709function tpl_action($type, $link = false, $wrapper = false, $return = false, $pre = '', $suf = '', $inner = '')
710{
711    dbg_deprecated('see devel:menus');
712    $out = '';
713    if ($link) {
714        $out .= tpl_actionlink($type, $pre, $suf, $inner, true);
715    } else {
716        $out .= tpl_button($type, true);
717    }
718    if ($out && $wrapper) $out = "<$wrapper>$out</$wrapper>";
719
720    if ($return) return $out;
721    echo $out;
722    return (bool)$out;
723}
724
725/**
726 * Print the search form
727 *
728 * If the first parameter is given a div with the ID 'qsearch_out' will
729 * be added which instructs the ajax pagequicksearch to kick in and place
730 * its output into this div. The second parameter controls the propritary
731 * attribute autocomplete. If set to false this attribute will be set with an
732 * value of "off" to instruct the browser to disable it's own built in
733 * autocompletion feature (MSIE and Firefox)
734 *
735 * @param bool $ajax
736 * @param bool $autocomplete
737 * @return bool
738 *
739 * @author Andreas Gohr <andi@splitbrain.org>
740 */
741function tpl_searchform($ajax = true, $autocomplete = true)
742{
743    global $lang;
744    global $ACT;
745    global $QUERY;
746    global $ID;
747
748    // don't print the search form if search action has been disabled
749    if (!actionOK('search')) return false;
750
751    $searchForm = new Form([
752        'action' => wl(),
753        'method' => 'get',
754        'role' => 'search',
755        'class' => 'search',
756        'id' => 'dw__search',
757    ], true);
758    $searchForm->addTagOpen('div')->addClass('no');
759    $searchForm->setHiddenField('do', 'search');
760    $searchForm->setHiddenField('id', $ID);
761    $searchForm->addTextInput('q')
762        ->addClass('edit')
763        ->attrs([
764            'title' => '[F]',
765            'accesskey' => 'f',
766            'placeholder' => $lang['btn_search'],
767            'autocomplete' => $autocomplete ? 'on' : 'off',
768        ])
769        ->id('qsearch__in')
770        ->val($ACT === 'search' ? $QUERY : '')
771        ->useInput(false);
772    $searchForm->addButton('', $lang['btn_search'])->attrs([
773        'type' => 'submit',
774        'title' => $lang['btn_search'],
775    ]);
776    if ($ajax) {
777        $searchForm->addTagOpen('div')->id('qsearch__out')->addClass('ajax_qsearch JSpopup');
778        $searchForm->addTagClose('div');
779    }
780    $searchForm->addTagClose('div');
781
782    echo $searchForm->toHTML('QuickSearch');
783
784    return true;
785}
786
787/**
788 * Print the breadcrumbs trace
789 *
790 * @param string $sep Separator between entries
791 * @param bool $return return or print
792 * @return bool|string
793 *
794 * @author Andreas Gohr <andi@splitbrain.org>
795 */
796function tpl_breadcrumbs($sep = null, $return = false)
797{
798    global $lang;
799    global $conf;
800
801    //check if enabled
802    if (!$conf['breadcrumbs']) return false;
803
804    //set default
805    if (is_null($sep)) $sep = '•';
806
807    $out = '';
808
809    $crumbs = breadcrumbs(); //setup crumb trace
810
811    $crumbs_sep = ' <span class="bcsep">' . $sep . '</span> ';
812
813    //render crumbs, highlight the last one
814    $out .= '<span class="bchead">' . $lang['breadcrumb'] . '</span>';
815    $last = count($crumbs);
816    $i = 0;
817    foreach ($crumbs as $id => $name) {
818        $i++;
819        $out .= $crumbs_sep;
820        if ($i == $last) $out .= '<span class="curid">';
821        $out .= '<bdi>' . tpl_link(wl($id), hsc($name), 'class="breadcrumbs" title="' . $id . '"', true) . '</bdi>';
822        if ($i == $last) $out .= '</span>';
823    }
824    if ($return) return $out;
825    echo $out;
826    return (bool)$out;
827}
828
829/**
830 * Hierarchical breadcrumbs
831 *
832 * This code was suggested as replacement for the usual breadcrumbs.
833 * It only makes sense with a deep site structure.
834 *
835 * @param string $sep Separator between entries
836 * @param bool $return return or print
837 * @return bool|string
838 *
839 * @todo   May behave strangely in RTL languages
840 * @author <fredrik@averpil.com>
841 * @author Andreas Gohr <andi@splitbrain.org>
842 * @author Nigel McNie <oracle.shinoda@gmail.com>
843 * @author Sean Coates <sean@caedmon.net>
844 */
845function tpl_youarehere($sep = null, $return = false)
846{
847    global $conf;
848    global $ID;
849    global $lang;
850
851    // check if enabled
852    if (!$conf['youarehere']) return false;
853
854    //set default
855    if (is_null($sep)) $sep = ' » ';
856
857    $out = '';
858
859    $parts = explode(':', $ID);
860    $count = count($parts);
861
862    $out .= '<span class="bchead">' . $lang['youarehere'] . ' </span>';
863
864    // always print the startpage
865    $out .= '<span class="home">' . tpl_pagelink(':' . $conf['start'], null, true) . '</span>';
866
867    // print intermediate namespace links
868    $part = '';
869    for ($i = 0; $i < $count - 1; $i++) {
870        $part .= $parts[$i] . ':';
871        $page = $part;
872        if ($page == $conf['start']) continue; // Skip startpage
873
874        // output
875        $out .= $sep . tpl_pagelink($page, null, true);
876    }
877
878    // print current page, skipping start page, skipping for namespace index
879    if (isset($page)) {
880        $page = (new PageResolver('root'))->resolveId($page);
881        if ($page == $part . $parts[$i]) {
882            if ($return) return $out;
883            echo $out;
884            return true;
885        }
886    }
887    $page = $part . $parts[$i];
888    if ($page == $conf['start']) {
889        if ($return) return $out;
890        echo $out;
891        return true;
892    }
893    $out .= $sep;
894    $out .= tpl_pagelink($page, null, true);
895    if ($return) return $out;
896    echo $out;
897    return (bool)$out;
898}
899
900/**
901 * Print info if the user is logged in
902 * and show full name in that case
903 *
904 * Could be enhanced with a profile link in future?
905 *
906 * @return bool
907 *
908 * @author Andreas Gohr <andi@splitbrain.org>
909 */
910function tpl_userinfo()
911{
912    global $lang;
913    /** @var Input $INPUT */
914    global $INPUT;
915
916    if ($INPUT->server->str('REMOTE_USER')) {
917        echo $lang['loggedinas'] . ' ' . userlink();
918        return true;
919    }
920    return false;
921}
922
923/**
924 * Print some info about the current page
925 *
926 * @param bool $ret return content instead of printing it
927 * @return bool|string
928 *
929 * @author Andreas Gohr <andi@splitbrain.org>
930 */
931function tpl_pageinfo($ret = false)
932{
933    global $conf;
934    global $lang;
935    global $INFO;
936    global $ID;
937
938    // return if we are not allowed to view the page
939    if (!auth_quickaclcheck($ID)) {
940        return false;
941    }
942
943    // prepare date and path
944    $fn = $INFO['filepath'];
945    if (!$conf['fullpath']) {
946        if ($INFO['rev']) {
947            $fn = str_replace($conf['olddir'] . '/', '', $fn);
948        } else {
949            $fn = str_replace($conf['datadir'] . '/', '', $fn);
950        }
951    }
952    $fn = utf8_decodeFN($fn);
953    $dateLocal = dformat($INFO['lastmod']);
954    $dateIso = date(DATE_ISO8601, $INFO['lastmod']);
955
956    // print it
957    if ($INFO['exists']) {
958        $out = '<bdi>' . $fn . '</bdi>';
959        $out .= ' · ';
960        $out .= $lang['lastmod'];
961        $out .= ' ';
962        $out .= '<time datetime="' . $dateIso . '">' . $dateLocal . '</time>';
963        if ($INFO['editor']) {
964            $out .= ' ' . $lang['by'] . ' ';
965            $out .= '<bdi>' . editorinfo($INFO['editor']) . '</bdi>';
966        } else {
967            $out .= ' (' . $lang['external_edit'] . ')';
968        }
969        if ($INFO['locked']) {
970            $out .= ' · ';
971            $out .= $lang['lockedby'];
972            $out .= ' ';
973            $out .= '<bdi>' . editorinfo($INFO['locked']) . '</bdi>';
974        }
975        if ($ret) {
976            return $out;
977        } else {
978            echo $out;
979            return true;
980        }
981    }
982    return false;
983}
984
985/**
986 * Prints or returns the name of the given page (current one if none given).
987 *
988 * If useheading is enabled this will use the first headline else
989 * the given ID is used.
990 *
991 * @param string $id page id
992 * @param bool $ret return content instead of printing
993 * @return bool|string
994 *
995 * @author Andreas Gohr <andi@splitbrain.org>
996 */
997function tpl_pagetitle($id = null, $ret = false)
998{
999    global $ACT, $conf, $lang;
1000
1001    if (is_null($id)) {
1002        global $ID;
1003        $id = $ID;
1004    }
1005
1006    $name = $id;
1007    if (useHeading('navigation')) {
1008        $first_heading = p_get_first_heading($id);
1009        if ($first_heading) $name = $first_heading;
1010    }
1011
1012    // default page title is the page name, modify with the current action
1013    switch ($ACT) {
1014        // admin functions
1015        case 'admin':
1016            $page_title = $lang['btn_admin'];
1017            // try to get the plugin name
1018            /** @var AdminPlugin $plugin */
1019            if ($plugin = plugin_getRequestAdminPlugin()) {
1020                $plugin_title = $plugin->getMenuText($conf['lang']);
1021                $page_title = $plugin_title ?: $plugin->getPluginName();
1022            }
1023            break;
1024
1025        // show action as title
1026        case 'login':
1027        case 'profile':
1028        case 'register':
1029        case 'resendpwd':
1030        case 'index':
1031        case 'search':
1032            $page_title = $lang['btn_' . $ACT];
1033            break;
1034
1035        // add pen during editing
1036        case 'edit':
1037        case 'preview':
1038            $page_title = "✎ " . $name;
1039            break;
1040
1041        // add action to page name
1042        case 'revisions':
1043            $page_title = $name . ' - ' . $lang['btn_revs'];
1044            break;
1045
1046        // add action to page name
1047        case 'backlink':
1048        case 'recent':
1049        case 'subscribe':
1050            $page_title = $name . ' - ' . $lang['btn_' . $ACT];
1051            break;
1052
1053        default: // SHOW and anything else not included
1054            $page_title = $name;
1055    }
1056
1057    if ($ret) {
1058        return hsc($page_title);
1059    } else {
1060        echo hsc($page_title);
1061        return true;
1062    }
1063}
1064
1065/**
1066 * Returns the requested EXIF/IPTC tag from the current image
1067 *
1068 * If $tags is an array all given tags are tried until a
1069 * value is found. If no value is found $alt is returned.
1070 *
1071 * Which texts are known is defined in the functions _exifTagNames
1072 * and _iptcTagNames() in inc/jpeg.php (You need to prepend IPTC
1073 * to the names of the latter one)
1074 *
1075 * Only allowed in: detail.php
1076 *
1077 * @param array|string $tags tag or array of tags to try
1078 * @param string $alt alternative output if no data was found
1079 * @param null|string $src the image src, uses global $SRC if not given
1080 * @return string
1081 *
1082 * @author Andreas Gohr <andi@splitbrain.org>
1083 */
1084function tpl_img_getTag($tags, $alt = '', $src = null)
1085{
1086    // Init Exif Reader
1087    global $SRC, $imgMeta;
1088
1089    if (is_null($src)) $src = $SRC;
1090    if (is_null($src)) return $alt;
1091
1092    if (!isset($imgMeta)) {
1093        $imgMeta = new JpegMeta($src);
1094    }
1095    if ($imgMeta === false) return $alt;
1096    $info = cleanText($imgMeta->getField($tags));
1097    if (!$info) return $alt;
1098    return $info;
1099}
1100
1101
1102/**
1103 * Garbage collects up the open JpegMeta object.
1104 */
1105function tpl_img_close()
1106{
1107    global $imgMeta;
1108    $imgMeta = null;
1109}
1110
1111/**
1112 * Prints a html description list of the metatags of the current image
1113 */
1114function tpl_img_meta()
1115{
1116    global $lang;
1117
1118    $tags = tpl_get_img_meta();
1119
1120    echo '<dl>';
1121    foreach ($tags as $tag) {
1122        $label = $lang[$tag['langkey']];
1123        if (!$label) $label = $tag['langkey'] . ':';
1124
1125        echo '<dt>' . $label . '</dt><dd>';
1126        if ($tag['type'] == 'date') {
1127            echo dformat($tag['value']);
1128        } else {
1129            echo hsc($tag['value']);
1130        }
1131        echo '</dd>';
1132    }
1133    echo '</dl>';
1134}
1135
1136/**
1137 * Returns metadata as configured in mediameta config file, ready for creating html
1138 *
1139 * @return array with arrays containing the entries:
1140 *   - string langkey  key to lookup in the $lang var, if not found printed as is
1141 *   - string type     type of value
1142 *   - string value    tag value (unescaped)
1143 */
1144function tpl_get_img_meta()
1145{
1146
1147    $config_files = getConfigFiles('mediameta');
1148    foreach ($config_files as $config_file) {
1149        if (file_exists($config_file)) {
1150            include($config_file);
1151        }
1152    }
1153    $tags = [];
1154    foreach ($fields as $tag) {
1155        $t = [];
1156        if (!empty($tag[0])) {
1157            $t = [$tag[0]];
1158        }
1159        if (isset($tag[3]) && is_array($tag[3])) {
1160            $t = array_merge($t, $tag[3]);
1161        }
1162        $value = tpl_img_getTag($t);
1163        if ($value) {
1164            $tags[] = ['langkey' => $tag[1], 'type' => $tag[2], 'value' => $value];
1165        }
1166    }
1167    return $tags;
1168}
1169
1170/**
1171 * Prints the image with a link to the full sized version
1172 *
1173 * Only allowed in: detail.php
1174 *
1175 * @triggers TPL_IMG_DISPLAY
1176 * @param int $maxwidth - maximal width of the image
1177 * @param int $maxheight - maximal height of the image
1178 * @param bool $link - link to the orginal size?
1179 * @param array $params - additional image attributes
1180 * @return bool Result of TPL_IMG_DISPLAY
1181 */
1182function tpl_img($maxwidth = 0, $maxheight = 0, $link = true, $params = null)
1183{
1184    global $IMG;
1185    /** @var Input $INPUT */
1186    global $INPUT;
1187    global $REV;
1188
1189    // rotation-aware bbox fit; returns [0, 0] for SVG / unreadable files
1190    [$w, $h] = (new MediaFile($IMG, $REV))->getDisplayDimensions($maxwidth, $maxheight, false);
1191
1192    //prepare URLs
1193    $srcParams = ['cache' => $INPUT->str('cache'), 'rev' => $REV];
1194    if ($maxwidth) $srcParams['w'] = $maxwidth;
1195    if ($maxheight) $srcParams['h'] = $maxheight;
1196    if ($maxwidth && $maxheight) $srcParams['fit'] = 1;
1197    $url = ml($IMG, ['cache' => $INPUT->str('cache'), 'rev' => $REV], true, '&');
1198    $src = ml($IMG, $srcParams, true, '&');
1199
1200    //prepare attributes
1201    $alt = tpl_img_getTag('Simple.Title');
1202    if (is_null($params)) {
1203        $p = [];
1204    } else {
1205        $p = $params;
1206    }
1207    if ($w) $p['width'] = $w;
1208    if ($h) $p['height'] = $h;
1209    $p['class'] = 'img_detail';
1210    if ($alt) {
1211        $p['alt'] = $alt;
1212        $p['title'] = $alt;
1213    } else {
1214        $p['alt'] = '';
1215    }
1216    $p['src'] = $src;
1217
1218    $data = ['url' => ($link ? $url : null), 'params' => $p];
1219    return Event::createAndTrigger('TPL_IMG_DISPLAY', $data, '_tpl_img_action', true);
1220}
1221
1222/**
1223 * Default action for TPL_IMG_DISPLAY
1224 *
1225 * @param array $data
1226 * @return bool
1227 */
1228function _tpl_img_action($data)
1229{
1230    global $lang;
1231    $p = buildAttributes($data['params']);
1232
1233    if ($data['url']) echo '<a href="' . hsc($data['url']) . '" title="' . $lang['mediaview'] . '">';
1234    echo '<img ' . $p . '/>';
1235    if ($data['url']) echo '</a>';
1236    return true;
1237}
1238
1239/**
1240 * This function inserts a small gif which in reality is the indexer function.
1241 *
1242 * Should be called somewhere at the very end of the main.php template
1243 *
1244 * @return bool
1245 */
1246function tpl_indexerWebBug()
1247{
1248    global $ID;
1249
1250    $p = [];
1251    $p['src'] = DOKU_BASE . 'lib/exe/taskrunner.php?id=' . rawurlencode($ID) .
1252        '&' . time();
1253    $p['width'] = 2; //no more 1x1 px image because we live in times of ad blockers...
1254    $p['height'] = 1;
1255    $p['alt'] = '';
1256    $att = buildAttributes($p);
1257    echo "<img $att />";
1258    return true;
1259}
1260
1261/**
1262 * tpl_getConf($id)
1263 *
1264 * use this function to access template configuration variables
1265 *
1266 * @param string $id name of the value to access
1267 * @param mixed $notset what to return if the setting is not available
1268 * @return mixed
1269 */
1270function tpl_getConf($id, $notset = false)
1271{
1272    global $conf;
1273    static $tpl_configloaded = false;
1274
1275    $tpl = $conf['template'];
1276
1277    if (!$tpl_configloaded) {
1278        $tconf = tpl_loadConfig();
1279        if ($tconf !== false) {
1280            foreach ($tconf as $key => $value) {
1281                if (isset($conf['tpl'][$tpl][$key])) continue;
1282                $conf['tpl'][$tpl][$key] = $value;
1283            }
1284            $tpl_configloaded = true;
1285        }
1286    }
1287
1288    return $conf['tpl'][$tpl][$id] ?? $notset;
1289}
1290
1291/**
1292 * tpl_loadConfig()
1293 *
1294 * reads all template configuration variables
1295 * this function is automatically called by tpl_getConf()
1296 *
1297 * @return false|array
1298 */
1299function tpl_loadConfig()
1300{
1301
1302    $file = tpl_incdir() . '/conf/default.php';
1303    $conf = [];
1304
1305    if (!file_exists($file)) return false;
1306
1307    // load default config file
1308    include($file);
1309
1310    return $conf;
1311}
1312
1313// language methods
1314
1315/**
1316 * tpl_getLang($id)
1317 *
1318 * use this function to access template language variables
1319 *
1320 * @param string $id key of language string
1321 * @return string
1322 */
1323function tpl_getLang($id)
1324{
1325    static $lang = [];
1326
1327    if (count($lang) === 0) {
1328        global $conf, $config_cascade; // definitely don't invoke "global $lang"
1329
1330        $path = tpl_incdir() . 'lang/';
1331
1332        $lang = [];
1333
1334        // don't include once
1335        @include($path . 'en/lang.php');
1336        foreach ($config_cascade['lang']['template'] as $config_file) {
1337            if (file_exists($config_file . $conf['template'] . '/en/lang.php')) {
1338                include($config_file . $conf['template'] . '/en/lang.php');
1339            }
1340        }
1341
1342        if ($conf['lang'] != 'en') {
1343            @include($path . $conf['lang'] . '/lang.php');
1344            foreach ($config_cascade['lang']['template'] as $config_file) {
1345                if (file_exists($config_file . $conf['template'] . '/' . $conf['lang'] . '/lang.php')) {
1346                    include($config_file . $conf['template'] . '/' . $conf['lang'] . '/lang.php');
1347                }
1348            }
1349        }
1350    }
1351    return $lang[$id] ?? '';
1352}
1353
1354/**
1355 * Retrieve a language dependent file and pass to xhtml renderer for display
1356 * template equivalent of p_locale_xhtml()
1357 *
1358 * @param string $id id of language dependent wiki page
1359 * @return  string     parsed contents of the wiki page in xhtml format
1360 */
1361function tpl_locale_xhtml($id)
1362{
1363    return p_cached_output(tpl_localeFN($id));
1364}
1365
1366/**
1367 * Prepends appropriate path for a language dependent filename
1368 *
1369 * @param string $id id of localized text
1370 * @return string wiki text
1371 */
1372function tpl_localeFN($id)
1373{
1374    $path = tpl_incdir() . 'lang/';
1375    global $conf;
1376    $file = DOKU_CONF . 'template_lang/' . $conf['template'] . '/' . $conf['lang'] . '/' . $id . '.txt';
1377    if (!file_exists($file)) {
1378        $file = $path . $conf['lang'] . '/' . $id . '.txt';
1379        if (!file_exists($file)) {
1380            //fall back to english
1381            $file = $path . 'en/' . $id . '.txt';
1382        }
1383    }
1384    return $file;
1385}
1386
1387/**
1388 * prints the "main content" in the mediamanager popup
1389 *
1390 * Depending on the user's actions this may be a list of
1391 * files in a namespace, the meta editing dialog or
1392 * a message of referencing pages
1393 *
1394 * Only allowed in mediamanager.php
1395 *
1396 * @triggers MEDIAMANAGER_CONTENT_OUTPUT
1397 * @param bool $fromajax - set true when calling this function via ajax
1398 * @param string $sort
1399 *
1400 * @author Andreas Gohr <andi@splitbrain.org>
1401 */
1402function tpl_mediaContent($fromajax = false, $sort = 'natural')
1403{
1404    global $IMG;
1405    global $AUTH;
1406    global $INUSE;
1407    global $NS;
1408    global $JUMPTO;
1409    /** @var Input $INPUT */
1410    global $INPUT;
1411
1412    $do = $INPUT->extract('do')->str('do');
1413    if (in_array($do, ['save', 'cancel'])) $do = '';
1414
1415    if (!$do) {
1416        if ($INPUT->bool('edit')) {
1417            $do = 'metaform';
1418        } elseif (is_array($INUSE)) {
1419            $do = 'filesinuse';
1420        } else {
1421            $do = 'filelist';
1422        }
1423    }
1424
1425    // output the content pane, wrapped in an event.
1426    if (!$fromajax) echo '<div id="media__content">';
1427    $data = ['do' => $do];
1428    $evt = new Event('MEDIAMANAGER_CONTENT_OUTPUT', $data);
1429    if ($evt->advise_before()) {
1430        $do = $data['do'];
1431        if ($do == 'filesinuse') {
1432            media_filesinuse($INUSE, $IMG);
1433        } elseif ($do == 'filelist') {
1434            media_filelist($NS, $AUTH, $JUMPTO, false, $sort);
1435        } elseif ($do == 'searchlist') {
1436            media_searchlist($INPUT->str('q'), $NS, $AUTH);
1437        } else {
1438            msg('Unknown action ' . hsc($do), -1);
1439        }
1440    }
1441    $evt->advise_after();
1442    unset($evt);
1443    if (!$fromajax) echo '</div>';
1444}
1445
1446/**
1447 * Prints the central column in full-screen media manager
1448 * Depending on the opened tab this may be a list of
1449 * files in a namespace, upload form or search form
1450 *
1451 * @author Kate Arzamastseva <pshns@ukr.net>
1452 */
1453function tpl_mediaFileList()
1454{
1455    global $AUTH;
1456    global $NS;
1457    global $JUMPTO;
1458    global $lang;
1459    /** @var Input $INPUT */
1460    global $INPUT;
1461
1462    $opened_tab = $INPUT->str('tab_files');
1463    if (!$opened_tab || !in_array($opened_tab, ['files', 'upload', 'search'])) $opened_tab = 'files';
1464    if ($INPUT->str('mediado') == 'update') $opened_tab = 'upload';
1465
1466    echo '<h2 class="a11y">' . $lang['mediaselect'] . '</h2>' . NL;
1467
1468    media_tabs_files($opened_tab);
1469
1470    echo '<div class="panelHeader">' . NL;
1471    echo '<h3>';
1472    $tabTitle = $NS ?: '[' . $lang['mediaroot'] . ']';
1473    printf($lang['media_' . $opened_tab], '<strong>' . hsc($tabTitle) . '</strong>');
1474    echo '</h3>' . NL;
1475    if ($opened_tab === 'search' || $opened_tab === 'files') {
1476        media_tab_files_options();
1477    }
1478    echo '</div>' . NL;
1479
1480    echo '<div class="panelContent">' . NL;
1481    if ($opened_tab == 'files') {
1482        media_tab_files($NS, $AUTH, $JUMPTO);
1483    } elseif ($opened_tab == 'upload') {
1484        media_tab_upload($NS, $AUTH, $JUMPTO);
1485    } elseif ($opened_tab == 'search') {
1486        media_tab_search($NS, $AUTH);
1487    }
1488    echo '</div>' . NL;
1489}
1490
1491/**
1492 * Prints the third column in full-screen media manager
1493 * Depending on the opened tab this may be details of the
1494 * selected file, the meta editing dialog or
1495 * list of file revisions
1496 *
1497 * @param string $image
1498 * @param boolean $rev
1499 *
1500 * @author Kate Arzamastseva <pshns@ukr.net>
1501 */
1502function tpl_mediaFileDetails($image, $rev)
1503{
1504    global $conf, $DEL, $lang;
1505    /** @var Input $INPUT */
1506    global $INPUT;
1507
1508    $removed = (
1509        !file_exists(mediaFN($image)) &&
1510        file_exists(mediaMetaFN($image, '.changes')) &&
1511        $conf['mediarevisions']
1512    );
1513    if (!$image || (!file_exists(mediaFN($image)) && !$removed) || $DEL) return;
1514    if ($rev && !file_exists(mediaFN($image, $rev))) $rev = false;
1515    $ns = getNS($image);
1516    $do = $INPUT->str('mediado');
1517
1518    $opened_tab = $INPUT->str('tab_details');
1519
1520    $tab_array = ['view'];
1521    [, $mime] = mimetype($image);
1522    if ($mime == 'image/jpeg') {
1523        $tab_array[] = 'edit';
1524    }
1525    if ($conf['mediarevisions']) {
1526        $tab_array[] = 'history';
1527    }
1528
1529    if (!$opened_tab || !in_array($opened_tab, $tab_array)) $opened_tab = 'view';
1530    if ($INPUT->bool('edit')) $opened_tab = 'edit';
1531    if ($do == 'restore') $opened_tab = 'view';
1532
1533    media_tabs_details($image, $opened_tab);
1534
1535    echo '<div class="panelHeader"><h3>';
1536    [$ext] = mimetype($image, false);
1537    $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1538    $class = 'select mediafile mf_' . $class;
1539
1540    $attributes = $rev ? ['rev' => $rev] : [];
1541    $tabTitle = sprintf(
1542        '<strong><a href="%s" class="%s" title="%s">%s</a></strong>',
1543        ml($image, $attributes),
1544        $class,
1545        $lang['mediaview'],
1546        $image
1547    );
1548    if ($opened_tab === 'view' && $rev) {
1549        printf($lang['media_viewold'], $tabTitle, dformat($rev));
1550    } else {
1551        printf($lang['media_' . $opened_tab], $tabTitle);
1552    }
1553
1554    echo '</h3></div>' . NL;
1555
1556    echo '<div class="panelContent">' . NL;
1557
1558    if ($opened_tab == 'view') {
1559        media_tab_view($image, $ns, null, $rev);
1560    } elseif ($opened_tab == 'edit' && !$removed) {
1561        media_tab_edit($image, $ns);
1562    } elseif ($opened_tab == 'history' && $conf['mediarevisions']) {
1563        media_tab_history($image, $ns);
1564    }
1565
1566    echo '</div>' . NL;
1567}
1568
1569/**
1570 * prints the namespace tree in the mediamanager popup
1571 *
1572 * Only allowed in mediamanager.php
1573 *
1574 * @author Andreas Gohr <andi@splitbrain.org>
1575 */
1576function tpl_mediaTree()
1577{
1578    global $NS;
1579    echo '<div id="media__tree">';
1580    media_nstree($NS);
1581    echo '</div>';
1582}
1583
1584/**
1585 * Print a dropdown menu with all DokuWiki actions
1586 *
1587 * Note: this will not use any pretty URLs
1588 *
1589 * @param string $empty empty option label
1590 * @param string $button submit button label
1591 *
1592 * @author Andreas Gohr <andi@splitbrain.org>
1593 * @deprecated 2017-09-01 see devel:menus
1594 */
1595function tpl_actiondropdown($empty = '', $button = '&gt;')
1596{
1597    dbg_deprecated('see devel:menus');
1598    $menu = new MobileMenu();
1599    echo $menu->getDropdown($empty, $button);
1600}
1601
1602/**
1603 * Print a informational line about the used license
1604 *
1605 * @param string $img print image? (|button|badge)
1606 * @param bool $imgonly skip the textual description?
1607 * @param bool $return when true don't print, but return HTML
1608 * @param bool $wrap wrap in div with class="license"?
1609 * @return string
1610 *
1611 * @author Andreas Gohr <andi@splitbrain.org>
1612 */
1613function tpl_license($img = 'badge', $imgonly = false, $return = false, $wrap = true)
1614{
1615    global $license;
1616    global $conf;
1617    global $lang;
1618    if (!$conf['license']) return '';
1619    if (!is_array($license[$conf['license']])) return '';
1620    $lic = $license[$conf['license']];
1621    $target = ($conf['target']['extern']) ? ' target="' . $conf['target']['extern'] . '"' : '';
1622
1623    $out = '';
1624    if ($wrap) $out .= '<div class="license">';
1625    if ($img) {
1626        $src = license_img($img);
1627        if ($src) {
1628            $out .= '<a href="' . $lic['url'] . '" rel="license"' . $target;
1629            $out .= '><img src="' . DOKU_BASE . $src . '" alt="' . $lic['name'] . '" /></a>';
1630            if (!$imgonly) $out .= ' ';
1631        }
1632    }
1633    if (!$imgonly) {
1634        $out .= $lang['license'] . ' ';
1635        $out .= '<bdi><a href="' . $lic['url'] . '" rel="license" class="urlextern"' . $target;
1636        $out .= '>' . $lic['name'] . '</a></bdi>';
1637    }
1638    if ($wrap) $out .= '</div>';
1639
1640    if ($return) return $out;
1641    echo $out;
1642    return '';
1643}
1644
1645/**
1646 * Includes the rendered HTML of a given page
1647 *
1648 * This function is useful to populate sidebars or similar features in a
1649 * template
1650 *
1651 * @param string $pageid The page name you want to include
1652 * @param bool $print Should the content be printed or returned only
1653 * @param bool $propagate Search higher namespaces, too?
1654 * @param bool $useacl Include the page only if the ACLs check out?
1655 * @return bool|null|string
1656 */
1657function tpl_include_page($pageid, $print = true, $propagate = false, $useacl = true)
1658{
1659    if ($propagate) {
1660        $pageid = page_findnearest($pageid, $useacl);
1661    } elseif ($useacl && auth_quickaclcheck($pageid) == AUTH_NONE) {
1662        return false;
1663    }
1664    if (!$pageid) return false;
1665
1666    global $TOC;
1667    $oldtoc = $TOC;
1668    $html = p_wiki_xhtml($pageid, '', false);
1669    $TOC = $oldtoc;
1670
1671    if ($print) echo $html;
1672    return $html;
1673}
1674
1675/**
1676 * Display the subscribe form
1677 *
1678 * @author Adrian Lang <lang@cosmocode.de>
1679 * @deprecated 2020-07-23
1680 */
1681function tpl_subscribe()
1682{
1683    dbg_deprecated(Subscribe::class . '::show()');
1684    (new Subscribe())->show();
1685}
1686
1687/**
1688 * Tries to send already created content right to the browser
1689 *
1690 * Wraps around ob_flush() and flush()
1691 *
1692 * @author Andreas Gohr <andi@splitbrain.org>
1693 */
1694function tpl_flush()
1695{
1696    if (ob_get_level() > 0) ob_flush();
1697    flush();
1698}
1699
1700/**
1701 * Tries to find a ressource file in the given locations.
1702 *
1703 * If a given location starts with a colon it is assumed to be a media
1704 * file, otherwise it is assumed to be relative to the current template
1705 *
1706 * @param string[] $search locations to look at
1707 * @param bool $abs if to use absolute URL
1708 * @param array    &$imginfo filled with getimagesize()
1709 * @param bool $fallback use fallback image if target isn't found or return 'false' if potential
1710 *                                false result is required
1711 * @return string
1712 *
1713 * @author Andreas  Gohr <andi@splitbrain.org>
1714 */
1715function tpl_getMediaFile($search, $abs = false, &$imginfo = null, $fallback = true)
1716{
1717    $img = '';
1718    $file = '';
1719    $ismedia = false;
1720    // loop through candidates until a match was found:
1721    foreach ($search as $img) {
1722        if (str_starts_with($img, ':')) {
1723            $file = mediaFN($img);
1724            $ismedia = true;
1725        } else {
1726            $file = tpl_incdir() . $img;
1727            $ismedia = false;
1728        }
1729
1730        if (file_exists($file)) break;
1731    }
1732
1733    // manage non existing target
1734    if (!file_exists($file)) {
1735        // give result for fallback image
1736        if ($fallback) {
1737            $file = DOKU_INC . 'lib/images/blank.gif';
1738            // stop process if false result is required (if $fallback is false)
1739        } else {
1740            return false;
1741        }
1742    }
1743
1744    // fetch image data if requested
1745    if (!is_null($imginfo)) {
1746        $imginfo = getimagesize($file);
1747    }
1748
1749    // build URL
1750    if ($ismedia) {
1751        $url = ml($img, '', true, '', $abs);
1752    } else {
1753        $url = tpl_basedir() . $img;
1754        if ($abs) $url = DOKU_URL . substr($url, strlen(DOKU_REL));
1755    }
1756
1757    return $url;
1758}
1759
1760/**
1761 * PHP include a file
1762 *
1763 * either from the conf directory if it exists, otherwise use
1764 * file in the template's root directory.
1765 *
1766 * The function honours config cascade settings and looks for the given
1767 * file next to the ´main´ config files, in the order protected, local,
1768 * default.
1769 *
1770 * Note: no escaping or sanity checking is done here. Never pass user input
1771 * to this function!
1772 *
1773 * @param string $file
1774 *
1775 * @author Andreas Gohr <andi@splitbrain.org>
1776 * @author Anika Henke <anika@selfthinker.org>
1777 */
1778function tpl_includeFile($file)
1779{
1780    global $config_cascade;
1781    foreach (['protected', 'local', 'default'] as $config_group) {
1782        if (empty($config_cascade['main'][$config_group])) continue;
1783        foreach ($config_cascade['main'][$config_group] as $conf_file) {
1784            $dir = dirname($conf_file);
1785            if (file_exists("$dir/$file")) {
1786                include("$dir/$file");
1787                return;
1788            }
1789        }
1790    }
1791
1792    // still here? try the template dir
1793    $file = tpl_incdir() . $file;
1794    if (file_exists($file)) {
1795        include($file);
1796    }
1797}
1798
1799/**
1800 * Returns <link> tag for various icon types (favicon|mobile|generic)
1801 *
1802 * @param array $types - list of icon types to display (favicon|mobile|generic)
1803 * @return string
1804 *
1805 * @author Anika Henke <anika@selfthinker.org>
1806 */
1807function tpl_favicon($types = ['favicon'])
1808{
1809
1810    $return = '';
1811
1812    foreach ($types as $type) {
1813        switch ($type) {
1814            case 'favicon':
1815                $look = [':wiki:favicon.ico', ':favicon.ico', 'images/favicon.ico'];
1816                $return .= '<link rel="shortcut icon" href="' . tpl_getMediaFile($look) . '" />' . NL;
1817                break;
1818            case 'mobile':
1819                $look = [':wiki:apple-touch-icon.png', ':apple-touch-icon.png', 'images/apple-touch-icon.png'];
1820                $return .= '<link rel="apple-touch-icon" href="' . tpl_getMediaFile($look) . '" />' . NL;
1821                break;
1822            case 'generic':
1823                // ideal world solution, which doesn't work in any browser yet
1824                $look = [':wiki:favicon.svg', ':favicon.svg', 'images/favicon.svg'];
1825                $return .= '<link rel="icon" href="' . tpl_getMediaFile($look) . '" type="image/svg+xml" />' . NL;
1826                break;
1827        }
1828    }
1829
1830    return $return;
1831}
1832
1833/**
1834 * Prints full-screen media manager
1835 *
1836 * @author Kate Arzamastseva <pshns@ukr.net>
1837 */
1838function tpl_media()
1839{
1840    global $NS, $IMG, $JUMPTO, $REV, $lang, $fullscreen, $INPUT;
1841    $fullscreen = true;
1842    require_once DOKU_INC . 'lib/exe/mediamanager.php';
1843
1844    $rev = '';
1845    $image = cleanID($INPUT->str('image'));
1846    if (isset($IMG)) $image = $IMG;
1847    if (isset($JUMPTO)) $image = $JUMPTO;
1848    if (isset($REV) && !$JUMPTO) $rev = $REV;
1849
1850    echo '<div id="mediamanager__page">' . NL;
1851    echo '<h1>' . $lang['btn_media'] . '</h1>' . NL;
1852    html_msgarea();
1853
1854    echo '<div class="panel namespaces">' . NL;
1855    echo '<h2>' . $lang['namespaces'] . '</h2>' . NL;
1856    echo '<div class="panelHeader">';
1857    echo $lang['media_namespaces'];
1858    echo '</div>' . NL;
1859
1860    echo '<div class="panelContent" id="media__tree">' . NL;
1861    media_nstree($NS);
1862    echo '</div>' . NL;
1863    echo '</div>' . NL;
1864
1865    echo '<div class="panel filelist">' . NL;
1866    tpl_mediaFileList();
1867    echo '</div>' . NL;
1868
1869    echo '<div class="panel file">' . NL;
1870    echo '<h2 class="a11y">' . $lang['media_file'] . '</h2>' . NL;
1871    tpl_mediaFileDetails($image, $rev);
1872    echo '</div>' . NL;
1873
1874    echo '</div>' . NL;
1875}
1876
1877/**
1878 * Return useful layout classes
1879 *
1880 * @return string
1881 *
1882 * @author Anika Henke <anika@selfthinker.org>
1883 */
1884function tpl_classes()
1885{
1886    global $ACT, $conf, $ID, $INFO;
1887    /** @var Input $INPUT */
1888    global $INPUT;
1889
1890    $classes = [
1891        'dokuwiki',
1892        'mode_' . $ACT,
1893        'tpl_' . $conf['template'],
1894        $INPUT->server->bool('REMOTE_USER') ? 'loggedIn' : '',
1895        (isset($INFO['exists']) && $INFO['exists']) ? '' : 'notFound',
1896        ($ID == $conf['start']) ? 'home' : ''
1897    ];
1898    return implode(' ', $classes);
1899}
1900
1901/**
1902 * Create event for tools menues
1903 *
1904 * @param string $toolsname name of menu
1905 * @param array $items
1906 * @param string $view e.g. 'main', 'detail', ...
1907 *
1908 * @author Anika Henke <anika@selfthinker.org>
1909 * @deprecated 2017-09-01 see devel:menus
1910 */
1911function tpl_toolsevent($toolsname, $items, $view = 'main')
1912{
1913    dbg_deprecated('see devel:menus');
1914    $data = ['view' => $view, 'items' => $items];
1915
1916    $hook = 'TEMPLATE_' . strtoupper($toolsname) . '_DISPLAY';
1917    $evt = new Event($hook, $data);
1918    if ($evt->advise_before()) {
1919        foreach ($evt->data['items'] as $html) echo $html;
1920    }
1921    $evt->advise_after();
1922}
1923