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