xref: /dokuwiki/inc/parser/xhtml.php (revision a087da71ea972d05ca659a1e9406c87aa7ae258b)
1<?php
2/**
3 * Renderer for XHTML output
4 *
5 * This is DokuWiki's main renderer used to display page content in the wiki
6 *
7 * @author Harry Fuecks <hfuecks@gmail.com>
8 * @author Andreas Gohr <andi@splitbrain.org>
9 *
10 */
11class Doku_Renderer_xhtml extends Doku_Renderer {
12    /** @var array store the table of contents */
13    public $toc = array();
14
15    /** @var array A stack of section edit data */
16    protected $sectionedits = array();
17    var $date_at = '';    // link pages and media against this revision
18
19    /** @var int last section edit id, used by startSectionEdit */
20    protected $lastsecid = 0;
21
22    /** @var array the list of headers used to create unique link ids */
23    protected $headers = array();
24
25    /** @var array a list of footnotes, list starts at 1! */
26    protected $footnotes = array();
27
28    /** @var int current section level */
29    protected $lastlevel = 0;
30    /** @var array section node tracker */
31    protected $node = array(0, 0, 0, 0, 0);
32
33    /** @var string temporary $doc store */
34    protected $store = '';
35
36    /** @var array global counter, for table classes etc. */
37    protected $_counter = array(); //
38
39    /** @var int counts the code and file blocks, used to provide download links */
40    protected $_codeblock = 0;
41
42    /** @var array list of allowed URL schemes */
43    protected $schemes = null;
44
45    /**
46     * Register a new edit section range
47     *
48     * @param int    $start  The byte position for the edit start
49     * @param array  $data   Associative array with section data:
50     *                       Key 'name': the section name/title
51     *                       Key 'target': the target for the section edit,
52     *                                     e.g. 'section' or 'table'
53     *                       Key 'hid': header id
54     *                       Key 'codeblockOffset': actual code block index
55     *                       Key 'start': set in startSectionEdit(),
56     *                                    do not set yourself
57     *                       Key 'range': calculated from 'start' and
58     *                                    $key in finishSectionEdit(),
59     *                                    do not set yourself
60     * @return string  A marker class for the starting HTML element
61     *
62     * @author Adrian Lang <lang@cosmocode.de>
63     */
64    public function startSectionEdit($start, $data) {
65        if (!is_array($data)) {
66            msg(
67                sprintf(
68                    'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
69                    hsc((string) $data)
70                ), -1
71            );
72
73            // @deprecated 2018-04-14, backward compatibility
74            $args = func_get_args();
75            $data = array();
76            if(isset($args[1])) $data['target'] = $args[1];
77            if(isset($args[2])) $data['name'] = $args[2];
78            if(isset($args[3])) $data['hid'] = $args[3];
79        }
80        $data['secid'] = ++$this->lastsecid;
81        $data['start'] = $start;
82        $this->sectionedits[] = $data;
83        return 'sectionedit'.$data['secid'];
84    }
85
86    /**
87     * Finish an edit section range
88     *
89     * @param int  $end     The byte position for the edit end; null for the rest of the page
90     *
91     * @author Adrian Lang <lang@cosmocode.de>
92     */
93    public function finishSectionEdit($end = null, $hid = null) {
94        $data = array_pop($this->sectionedits);
95        if(!is_null($end) && $end <= $data['start']) {
96            return;
97        }
98        if(!is_null($hid)) {
99            $data['hid'] .= $hid;
100        }
101        $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end);
102        unset($data['start']);
103        $this->doc .= '<!-- EDIT'.hsc(json_encode ($data)).' -->';
104    }
105
106    /**
107     * Returns the format produced by this renderer.
108     *
109     * @return string always 'xhtml'
110     */
111    function getFormat() {
112        return 'xhtml';
113    }
114
115    /**
116     * Initialize the document
117     */
118    function document_start() {
119        //reset some internals
120        $this->toc     = array();
121        $this->headers = array();
122    }
123
124    /**
125     * Finalize the document
126     */
127    function document_end() {
128        // Finish open section edits.
129        while(count($this->sectionedits) > 0) {
130            if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
131                // If there is only one section, do not write a section edit
132                // marker.
133                array_pop($this->sectionedits);
134            } else {
135                $this->finishSectionEdit();
136            }
137        }
138
139        if(count($this->footnotes) > 0) {
140            $this->doc .= '<div class="footnotes">'.DOKU_LF;
141
142            foreach($this->footnotes as $id => $footnote) {
143                // check its not a placeholder that indicates actual footnote text is elsewhere
144                if(substr($footnote, 0, 5) != "@@FNT") {
145
146                    // open the footnote and set the anchor and backlink
147                    $this->doc .= '<div class="fn">';
148                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
149                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
150
151                    // get any other footnotes that use the same markup
152                    $alt = array_keys($this->footnotes, "@@FNT$id");
153
154                    if(count($alt)) {
155                        foreach($alt as $ref) {
156                            // set anchor and backlink for the other footnotes
157                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
158                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
159                        }
160                    }
161
162                    // add footnote markup and close this footnote
163                    $this->doc .= '<div class="content">'.$footnote.'</div>';
164                    $this->doc .= '</div>'.DOKU_LF;
165                }
166            }
167            $this->doc .= '</div>'.DOKU_LF;
168        }
169
170        // Prepare the TOC
171        global $conf;
172        if(
173            $this->info['toc'] &&
174            is_array($this->toc) &&
175            $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']
176        ) {
177            global $TOC;
178            $TOC = $this->toc;
179        }
180
181        // make sure there are no empty paragraphs
182        $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
183    }
184
185    /**
186     * Add an item to the TOC
187     *
188     * @param string $id       the hash link
189     * @param string $text     the text to display
190     * @param int    $level    the nesting level
191     */
192    function toc_additem($id, $text, $level) {
193        global $conf;
194
195        //handle TOC
196        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
197            $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
198        }
199    }
200
201    /**
202     * Render a heading
203     *
204     * @param string $text  the text to display
205     * @param int    $level header level
206     * @param int    $pos   byte position in the original source
207     */
208    function header($text, $level, $pos) {
209        global $conf;
210
211        if(blank($text)) return; //skip empty headlines
212
213        $hid = $this->_headerToLink($text, true);
214
215        //only add items within configured levels
216        $this->toc_additem($hid, $text, $level);
217
218        // adjust $node to reflect hierarchy of levels
219        $this->node[$level - 1]++;
220        if($level < $this->lastlevel) {
221            for($i = 0; $i < $this->lastlevel - $level; $i++) {
222                $this->node[$this->lastlevel - $i - 1] = 0;
223            }
224        }
225        $this->lastlevel = $level;
226
227        if($level <= $conf['maxseclevel'] &&
228            count($this->sectionedits) > 0 &&
229            $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
230        ) {
231            $this->finishSectionEdit($pos - 1);
232        }
233
234        // write the header
235        $this->doc .= DOKU_LF.'<h'.$level;
236        if($level <= $conf['maxseclevel']) {
237            $data = array();
238            $data['target'] = 'section';
239            $data['name'] = $text;
240            $data['hid'] = $hid;
241            $data['codeblockOffset'] = $this->_codeblock;
242            $this->doc .= ' class="'.$this->startSectionEdit($pos, $data).'"';
243        }
244        $this->doc .= ' id="'.$hid.'">';
245        $this->doc .= $this->_xmlEntities($text);
246        $this->doc .= "</h$level>".DOKU_LF;
247    }
248
249    /**
250     * Open a new section
251     *
252     * @param int $level section level (as determined by the previous header)
253     */
254    function section_open($level) {
255        $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
256    }
257
258    /**
259     * Close the current section
260     */
261    function section_close() {
262        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
263    }
264
265    /**
266     * Render plain text data
267     *
268     * @param $text
269     */
270    function cdata($text) {
271        $this->doc .= $this->_xmlEntities($text);
272    }
273
274    /**
275     * Open a paragraph
276     */
277    function p_open() {
278        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
279    }
280
281    /**
282     * Close a paragraph
283     */
284    function p_close() {
285        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
286    }
287
288    /**
289     * Create a line break
290     */
291    function linebreak() {
292        $this->doc .= '<br/>'.DOKU_LF;
293    }
294
295    /**
296     * Create a horizontal line
297     */
298    function hr() {
299        $this->doc .= '<hr />'.DOKU_LF;
300    }
301
302    /**
303     * Start strong (bold) formatting
304     */
305    function strong_open() {
306        $this->doc .= '<strong>';
307    }
308
309    /**
310     * Stop strong (bold) formatting
311     */
312    function strong_close() {
313        $this->doc .= '</strong>';
314    }
315
316    /**
317     * Start emphasis (italics) formatting
318     */
319    function emphasis_open() {
320        $this->doc .= '<em>';
321    }
322
323    /**
324     * Stop emphasis (italics) formatting
325     */
326    function emphasis_close() {
327        $this->doc .= '</em>';
328    }
329
330    /**
331     * Start underline formatting
332     */
333    function underline_open() {
334        $this->doc .= '<em class="u">';
335    }
336
337    /**
338     * Stop underline formatting
339     */
340    function underline_close() {
341        $this->doc .= '</em>';
342    }
343
344    /**
345     * Start monospace formatting
346     */
347    function monospace_open() {
348        $this->doc .= '<code>';
349    }
350
351    /**
352     * Stop monospace formatting
353     */
354    function monospace_close() {
355        $this->doc .= '</code>';
356    }
357
358    /**
359     * Start a subscript
360     */
361    function subscript_open() {
362        $this->doc .= '<sub>';
363    }
364
365    /**
366     * Stop a subscript
367     */
368    function subscript_close() {
369        $this->doc .= '</sub>';
370    }
371
372    /**
373     * Start a superscript
374     */
375    function superscript_open() {
376        $this->doc .= '<sup>';
377    }
378
379    /**
380     * Stop a superscript
381     */
382    function superscript_close() {
383        $this->doc .= '</sup>';
384    }
385
386    /**
387     * Start deleted (strike-through) formatting
388     */
389    function deleted_open() {
390        $this->doc .= '<del>';
391    }
392
393    /**
394     * Stop deleted (strike-through) formatting
395     */
396    function deleted_close() {
397        $this->doc .= '</del>';
398    }
399
400    /**
401     * Callback for footnote start syntax
402     *
403     * All following content will go to the footnote instead of
404     * the document. To achieve this the previous rendered content
405     * is moved to $store and $doc is cleared
406     *
407     * @author Andreas Gohr <andi@splitbrain.org>
408     */
409    function footnote_open() {
410
411        // move current content to store and record footnote
412        $this->store = $this->doc;
413        $this->doc   = '';
414    }
415
416    /**
417     * Callback for footnote end syntax
418     *
419     * All rendered content is moved to the $footnotes array and the old
420     * content is restored from $store again
421     *
422     * @author Andreas Gohr
423     */
424    function footnote_close() {
425        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
426        static $fnid = 0;
427        // assign new footnote id (we start at 1)
428        $fnid++;
429
430        // recover footnote into the stack and restore old content
431        $footnote    = $this->doc;
432        $this->doc   = $this->store;
433        $this->store = '';
434
435        // check to see if this footnote has been seen before
436        $i = array_search($footnote, $this->footnotes);
437
438        if($i === false) {
439            // its a new footnote, add it to the $footnotes array
440            $this->footnotes[$fnid] = $footnote;
441        } else {
442            // seen this one before, save a placeholder
443            $this->footnotes[$fnid] = "@@FNT".($i);
444        }
445
446        // output the footnote reference and link
447        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
448    }
449
450    /**
451     * Open an unordered list
452     *
453     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
454     */
455    function listu_open($classes = null) {
456        $class = '';
457        if($classes !== null) {
458            if(is_array($classes)) $classes = join(' ', $classes);
459            $class = " class=\"$classes\"";
460        }
461        $this->doc .= "<ul$class>".DOKU_LF;
462    }
463
464    /**
465     * Close an unordered list
466     */
467    function listu_close() {
468        $this->doc .= '</ul>'.DOKU_LF;
469    }
470
471    /**
472     * Open an ordered list
473     *
474     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
475     */
476    function listo_open($classes = null) {
477        $class = '';
478        if($classes !== null) {
479            if(is_array($classes)) $classes = join(' ', $classes);
480            $class = " class=\"$classes\"";
481        }
482        $this->doc .= "<ol$class>".DOKU_LF;
483    }
484
485    /**
486     * Close an ordered list
487     */
488    function listo_close() {
489        $this->doc .= '</ol>'.DOKU_LF;
490    }
491
492    /**
493     * Open a list item
494     *
495     * @param int $level the nesting level
496     * @param bool $node true when a node; false when a leaf
497     */
498    function listitem_open($level, $node=false) {
499        $branching = $node ? ' node' : '';
500        $this->doc .= '<li class="level'.$level.$branching.'">';
501    }
502
503    /**
504     * Close a list item
505     */
506    function listitem_close() {
507        $this->doc .= '</li>'.DOKU_LF;
508    }
509
510    /**
511     * Start the content of a list item
512     */
513    function listcontent_open() {
514        $this->doc .= '<div class="li">';
515    }
516
517    /**
518     * Stop the content of a list item
519     */
520    function listcontent_close() {
521        $this->doc .= '</div>'.DOKU_LF;
522    }
523
524    /**
525     * Output unformatted $text
526     *
527     * Defaults to $this->cdata()
528     *
529     * @param string $text
530     */
531    function unformatted($text) {
532        $this->doc .= $this->_xmlEntities($text);
533    }
534
535    /**
536     * Execute PHP code if allowed
537     *
538     * @param  string $text      PHP code that is either executed or printed
539     * @param  string $wrapper   html element to wrap result if $conf['phpok'] is okff
540     *
541     * @author Andreas Gohr <andi@splitbrain.org>
542     */
543    function php($text, $wrapper = 'code') {
544        global $conf;
545
546        if($conf['phpok']) {
547            ob_start();
548            eval($text);
549            $this->doc .= ob_get_contents();
550            ob_end_clean();
551        } else {
552            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
553        }
554    }
555
556    /**
557     * Output block level PHP code
558     *
559     * If $conf['phpok'] is true this should evaluate the given code and append the result
560     * to $doc
561     *
562     * @param string $text The PHP code
563     */
564    function phpblock($text) {
565        $this->php($text, 'pre');
566    }
567
568    /**
569     * Insert HTML if allowed
570     *
571     * @param  string $text      html text
572     * @param  string $wrapper   html element to wrap result if $conf['htmlok'] is okff
573     *
574     * @author Andreas Gohr <andi@splitbrain.org>
575     */
576    function html($text, $wrapper = 'code') {
577        global $conf;
578
579        if($conf['htmlok']) {
580            $this->doc .= $text;
581        } else {
582            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
583        }
584    }
585
586    /**
587     * Output raw block-level HTML
588     *
589     * If $conf['htmlok'] is true this should add the code as is to $doc
590     *
591     * @param string $text The HTML
592     */
593    function htmlblock($text) {
594        $this->html($text, 'pre');
595    }
596
597    /**
598     * Start a block quote
599     */
600    function quote_open() {
601        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
602    }
603
604    /**
605     * Stop a block quote
606     */
607    function quote_close() {
608        $this->doc .= '</div></blockquote>'.DOKU_LF;
609    }
610
611    /**
612     * Output preformatted text
613     *
614     * @param string $text
615     */
616    function preformatted($text) {
617        $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
618    }
619
620    /**
621     * Display text as file content, optionally syntax highlighted
622     *
623     * @param string $text     text to show
624     * @param string $language programming language to use for syntax highlighting
625     * @param string $filename file path label
626     * @param array  $options  assoziative array with additional geshi options
627     */
628    function file($text, $language = null, $filename = null, $options=null) {
629        $this->_highlight('file', $text, $language, $filename, $options);
630    }
631
632    /**
633     * Display text as code content, optionally syntax highlighted
634     *
635     * @param string $text     text to show
636     * @param string $language programming language to use for syntax highlighting
637     * @param string $filename file path label
638     * @param array  $options  assoziative array with additional geshi options
639     */
640    function code($text, $language = null, $filename = null, $options=null) {
641        $this->_highlight('code', $text, $language, $filename, $options);
642    }
643
644    /**
645     * Use GeSHi to highlight language syntax in code and file blocks
646     *
647     * @author Andreas Gohr <andi@splitbrain.org>
648     * @param string $type     code|file
649     * @param string $text     text to show
650     * @param string $language programming language to use for syntax highlighting
651     * @param string $filename file path label
652     * @param array  $options  assoziative array with additional geshi options
653     */
654    function _highlight($type, $text, $language = null, $filename = null, $options = null) {
655        global $ID;
656        global $lang;
657        global $INPUT;
658
659        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
660
661        if($filename) {
662            // add icon
663            list($ext) = mimetype($filename, false);
664            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
665            $class = 'mediafile mf_'.$class;
666
667            $offset = 0;
668            if ($INPUT->has('codeblockOffset')) {
669                $offset = $INPUT->str('codeblockOffset');
670            }
671            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
672            $this->doc .= '<dt><a href="' .
673                exportlink(
674                    $ID,
675                    'code',
676                    array('codeblock' => $offset + $this->_codeblock)
677                ) . '" title="' . $lang['download'] . '" class="' . $class . '">';
678            $this->doc .= hsc($filename);
679            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
680        }
681
682        if($text{0} == "\n") {
683            $text = substr($text, 1);
684        }
685        if(substr($text, -1) == "\n") {
686            $text = substr($text, 0, -1);
687        }
688
689        if(empty($language)) { // empty is faster than is_null and can prevent '' string
690            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
691        } else {
692            $class = 'code'; //we always need the code class to make the syntax highlighting apply
693            if($type != 'code') $class .= ' '.$type;
694
695            $this->doc .= "<pre class=\"$class $language\">" .
696                p_xhtml_cached_geshi($text, $language, '', $options) .
697                '</pre>' . DOKU_LF;
698        }
699
700        if($filename) {
701            $this->doc .= '</dd></dl>'.DOKU_LF;
702        }
703
704        $this->_codeblock++;
705    }
706
707    /**
708     * Format an acronym
709     *
710     * Uses $this->acronyms
711     *
712     * @param string $acronym
713     */
714    function acronym($acronym) {
715
716        if(array_key_exists($acronym, $this->acronyms)) {
717
718            $title = $this->_xmlEntities($this->acronyms[$acronym]);
719
720            $this->doc .= '<abbr title="'.$title
721                .'">'.$this->_xmlEntities($acronym).'</abbr>';
722
723        } else {
724            $this->doc .= $this->_xmlEntities($acronym);
725        }
726    }
727
728    /**
729     * Format a smiley
730     *
731     * Uses $this->smiley
732     *
733     * @param string $smiley
734     */
735    function smiley($smiley) {
736        if(array_key_exists($smiley, $this->smileys)) {
737            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
738                '" class="icon" alt="'.
739                $this->_xmlEntities($smiley).'" />';
740        } else {
741            $this->doc .= $this->_xmlEntities($smiley);
742        }
743    }
744
745    /**
746     * Format an entity
747     *
748     * Entities are basically small text replacements
749     *
750     * Uses $this->entities
751     *
752     * @param string $entity
753     */
754    function entity($entity) {
755        if(array_key_exists($entity, $this->entities)) {
756            $this->doc .= $this->entities[$entity];
757        } else {
758            $this->doc .= $this->_xmlEntities($entity);
759        }
760    }
761
762    /**
763     * Typographically format a multiply sign
764     *
765     * Example: ($x=640, $y=480) should result in "640×480"
766     *
767     * @param string|int $x first value
768     * @param string|int $y second value
769     */
770    function multiplyentity($x, $y) {
771        $this->doc .= "$x&times;$y";
772    }
773
774    /**
775     * Render an opening single quote char (language specific)
776     */
777    function singlequoteopening() {
778        global $lang;
779        $this->doc .= $lang['singlequoteopening'];
780    }
781
782    /**
783     * Render a closing single quote char (language specific)
784     */
785    function singlequoteclosing() {
786        global $lang;
787        $this->doc .= $lang['singlequoteclosing'];
788    }
789
790    /**
791     * Render an apostrophe char (language specific)
792     */
793    function apostrophe() {
794        global $lang;
795        $this->doc .= $lang['apostrophe'];
796    }
797
798    /**
799     * Render an opening double quote char (language specific)
800     */
801    function doublequoteopening() {
802        global $lang;
803        $this->doc .= $lang['doublequoteopening'];
804    }
805
806    /**
807     * Render an closinging double quote char (language specific)
808     */
809    function doublequoteclosing() {
810        global $lang;
811        $this->doc .= $lang['doublequoteclosing'];
812    }
813
814    /**
815     * Render a CamelCase link
816     *
817     * @param string $link       The link name
818     * @param bool   $returnonly whether to return html or write to doc attribute
819     * @return void|string writes to doc attribute or returns html depends on $returnonly
820     *
821     * @see http://en.wikipedia.org/wiki/CamelCase
822     */
823    function camelcaselink($link, $returnonly = false) {
824        if($returnonly) {
825          return $this->internallink($link, $link, null, true);
826        } else {
827          $this->internallink($link, $link);
828        }
829    }
830
831    /**
832     * Render a page local link
833     *
834     * @param string $hash       hash link identifier
835     * @param string $name       name for the link
836     * @param bool   $returnonly whether to return html or write to doc attribute
837     * @return void|string writes to doc attribute or returns html depends on $returnonly
838     */
839    function locallink($hash, $name = null, $returnonly = false) {
840        global $ID;
841        $name  = $this->_getLinkTitle($name, $hash, $isImage);
842        $hash  = $this->_headerToLink($hash);
843        $title = $ID.' ↵';
844
845        $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
846        $doc .= $name;
847        $doc .= '</a>';
848
849        if($returnonly) {
850          return $doc;
851        } else {
852          $this->doc .= $doc;
853        }
854    }
855
856    /**
857     * Render an internal Wiki Link
858     *
859     * $search,$returnonly & $linktype are not for the renderer but are used
860     * elsewhere - no need to implement them in other renderers
861     *
862     * @author Andreas Gohr <andi@splitbrain.org>
863     * @param string      $id         pageid
864     * @param string|null $name       link name
865     * @param string|null $search     adds search url param
866     * @param bool        $returnonly whether to return html or write to doc attribute
867     * @param string      $linktype   type to set use of headings
868     * @return void|string writes to doc attribute or returns html depends on $returnonly
869     */
870    function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
871        global $conf;
872        global $ID;
873        global $INFO;
874
875        $params = '';
876        $parts  = explode('?', $id, 2);
877        if(count($parts) === 2) {
878            $id     = $parts[0];
879            $params = $parts[1];
880        }
881
882        // For empty $id we need to know the current $ID
883        // We need this check because _simpleTitle needs
884        // correct $id and resolve_pageid() use cleanID($id)
885        // (some things could be lost)
886        if($id === '') {
887            $id = $ID;
888        }
889
890        // default name is based on $id as given
891        $default = $this->_simpleTitle($id);
892
893        // now first resolve and clean up the $id
894        resolve_pageid(getNS($ID), $id, $exists, $this->date_at, true);
895
896        $link = array();
897        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
898        if(!$isImage) {
899            if($exists) {
900                $class = 'wikilink1';
901            } else {
902                $class       = 'wikilink2';
903                $link['rel'] = 'nofollow';
904            }
905        } else {
906            $class = 'media';
907        }
908
909        //keep hash anchor
910        @list($id, $hash) = explode('#', $id, 2);
911        if(!empty($hash)) $hash = $this->_headerToLink($hash);
912
913        //prepare for formating
914        $link['target'] = $conf['target']['wiki'];
915        $link['style']  = '';
916        $link['pre']    = '';
917        $link['suf']    = '';
918        // highlight link to current page
919        if($id == $INFO['id']) {
920            $link['pre'] = '<span class="curid">';
921            $link['suf'] = '</span>';
922        }
923        $link['more']   = '';
924        $link['class']  = $class;
925        if($this->date_at) {
926            $params = $params.'&at='.rawurlencode($this->date_at);
927        }
928        $link['url']    = wl($id, $params);
929        $link['name']   = $name;
930        $link['title']  = $id;
931        //add search string
932        if($search) {
933            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
934            if(is_array($search)) {
935                $search = array_map('rawurlencode', $search);
936                $link['url'] .= 's[]='.join('&amp;s[]=', $search);
937            } else {
938                $link['url'] .= 's='.rawurlencode($search);
939            }
940        }
941
942        //keep hash
943        if($hash) $link['url'] .= '#'.$hash;
944
945        //output formatted
946        if($returnonly) {
947            return $this->_formatLink($link);
948        } else {
949            $this->doc .= $this->_formatLink($link);
950        }
951    }
952
953    /**
954     * Render an external link
955     *
956     * @param string       $url        full URL with scheme
957     * @param string|array $name       name for the link, array for media file
958     * @param bool         $returnonly whether to return html or write to doc attribute
959     * @return void|string writes to doc attribute or returns html depends on $returnonly
960     */
961    function externallink($url, $name = null, $returnonly = false) {
962        global $conf;
963
964        $name = $this->_getLinkTitle($name, $url, $isImage);
965
966        // url might be an attack vector, only allow registered protocols
967        if(is_null($this->schemes)) $this->schemes = getSchemes();
968        list($scheme) = explode('://', $url);
969        $scheme = strtolower($scheme);
970        if(!in_array($scheme, $this->schemes)) $url = '';
971
972        // is there still an URL?
973        if(!$url) {
974            if($returnonly) {
975                return $name;
976            } else {
977                $this->doc .= $name;
978            }
979            return;
980        }
981
982        // set class
983        if(!$isImage) {
984            $class = 'urlextern';
985        } else {
986            $class = 'media';
987        }
988
989        //prepare for formating
990        $link = array();
991        $link['target'] = $conf['target']['extern'];
992        $link['style']  = '';
993        $link['pre']    = '';
994        $link['suf']    = '';
995        $link['more']   = '';
996        $link['class']  = $class;
997        $link['url']    = $url;
998        $link['rel']    = '';
999
1000        $link['name']  = $name;
1001        $link['title'] = $this->_xmlEntities($url);
1002        if($conf['relnofollow']) $link['rel'] .= ' nofollow';
1003        if($conf['target']['extern']) $link['rel'] .= ' noopener';
1004
1005        //output formatted
1006        if($returnonly) {
1007            return $this->_formatLink($link);
1008        } else {
1009            $this->doc .= $this->_formatLink($link);
1010        }
1011    }
1012
1013    /**
1014     * Render an interwiki link
1015     *
1016     * You may want to use $this->_resolveInterWiki() here
1017     *
1018     * @param string       $match      original link - probably not much use
1019     * @param string|array $name       name for the link, array for media file
1020     * @param string       $wikiName   indentifier (shortcut) for the remote wiki
1021     * @param string       $wikiUri    the fragment parsed from the original link
1022     * @param bool         $returnonly whether to return html or write to doc attribute
1023     * @return void|string writes to doc attribute or returns html depends on $returnonly
1024     */
1025    function interwikilink($match, $name = null, $wikiName, $wikiUri, $returnonly = false) {
1026        global $conf;
1027
1028        $link           = array();
1029        $link['target'] = $conf['target']['interwiki'];
1030        $link['pre']    = '';
1031        $link['suf']    = '';
1032        $link['more']   = '';
1033        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
1034        $link['rel']    = '';
1035
1036        //get interwiki URL
1037        $exists = null;
1038        $url    = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
1039
1040        if(!$isImage) {
1041            $class         = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
1042            $link['class'] = "interwiki iw_$class";
1043        } else {
1044            $link['class'] = 'media';
1045        }
1046
1047        //do we stay at the same server? Use local target
1048        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
1049            $link['target'] = $conf['target']['wiki'];
1050        }
1051        if($exists !== null && !$isImage) {
1052            if($exists) {
1053                $link['class'] .= ' wikilink1';
1054            } else {
1055                $link['class'] .= ' wikilink2';
1056                $link['rel'] .= ' nofollow';
1057            }
1058        }
1059        if($conf['target']['interwiki']) $link['rel'] .= ' noopener';
1060
1061        $link['url']   = $url;
1062        $link['title'] = htmlspecialchars($link['url']);
1063
1064        //output formatted
1065        if($returnonly) {
1066            return $this->_formatLink($link);
1067        } else {
1068            $this->doc .= $this->_formatLink($link);
1069        }
1070    }
1071
1072    /**
1073     * Link to windows share
1074     *
1075     * @param string       $url        the link
1076     * @param string|array $name       name for the link, array for media file
1077     * @param bool         $returnonly whether to return html or write to doc attribute
1078     * @return void|string writes to doc attribute or returns html depends on $returnonly
1079     */
1080    function windowssharelink($url, $name = null, $returnonly = false) {
1081        global $conf;
1082
1083        //simple setup
1084        $link = array();
1085        $link['target'] = $conf['target']['windows'];
1086        $link['pre']    = '';
1087        $link['suf']    = '';
1088        $link['style']  = '';
1089
1090        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
1091        if(!$isImage) {
1092            $link['class'] = 'windows';
1093        } else {
1094            $link['class'] = 'media';
1095        }
1096
1097        $link['title'] = $this->_xmlEntities($url);
1098        $url           = str_replace('\\', '/', $url);
1099        $url           = 'file:///'.$url;
1100        $link['url']   = $url;
1101
1102        //output formatted
1103        if($returnonly) {
1104            return $this->_formatLink($link);
1105        } else {
1106            $this->doc .= $this->_formatLink($link);
1107        }
1108    }
1109
1110    /**
1111     * Render a linked E-Mail Address
1112     *
1113     * Honors $conf['mailguard'] setting
1114     *
1115     * @param string       $address    Email-Address
1116     * @param string|array $name       name for the link, array for media file
1117     * @param bool         $returnonly whether to return html or write to doc attribute
1118     * @return void|string writes to doc attribute or returns html depends on $returnonly
1119     */
1120    function emaillink($address, $name = null, $returnonly = false) {
1121        global $conf;
1122        //simple setup
1123        $link           = array();
1124        $link['target'] = '';
1125        $link['pre']    = '';
1126        $link['suf']    = '';
1127        $link['style']  = '';
1128        $link['more']   = '';
1129
1130        $name = $this->_getLinkTitle($name, '', $isImage);
1131        if(!$isImage) {
1132            $link['class'] = 'mail';
1133        } else {
1134            $link['class'] = 'media';
1135        }
1136
1137        $address = $this->_xmlEntities($address);
1138        $address = obfuscate($address);
1139        $title   = $address;
1140
1141        if(empty($name)) {
1142            $name = $address;
1143        }
1144
1145        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
1146
1147        $link['url']   = 'mailto:'.$address;
1148        $link['name']  = $name;
1149        $link['title'] = $title;
1150
1151        //output formatted
1152        if($returnonly) {
1153            return $this->_formatLink($link);
1154        } else {
1155            $this->doc .= $this->_formatLink($link);
1156        }
1157    }
1158
1159    /**
1160     * Render an internal media file
1161     *
1162     * @param string $src       media ID
1163     * @param string $title     descriptive text
1164     * @param string $align     left|center|right
1165     * @param int    $width     width of media in pixel
1166     * @param int    $height    height of media in pixel
1167     * @param string $cache     cache|recache|nocache
1168     * @param string $linking   linkonly|detail|nolink
1169     * @param bool   $return    return HTML instead of adding to $doc
1170     * @return void|string writes to doc attribute or returns html depends on $return
1171     */
1172    function internalmedia($src, $title = null, $align = null, $width = null,
1173                           $height = null, $cache = null, $linking = null, $return = false) {
1174        global $ID;
1175        if (strpos($src, '#') !== false) {
1176            list($src, $hash) = explode('#', $src, 2);
1177        }
1178        resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true);
1179
1180        $noLink = false;
1181        $render = ($linking == 'linkonly') ? false : true;
1182        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1183
1184        list($ext, $mime) = mimetype($src, false);
1185        if(substr($mime, 0, 5) == 'image' && $render) {
1186            $link['url'] = ml(
1187                $src,
1188                array(
1189                    'id' => $ID,
1190                    'cache' => $cache,
1191                    'rev' => $this->_getLastMediaRevisionAt($src)
1192                ),
1193                ($linking == 'direct')
1194            );
1195        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1196            // don't link movies
1197            $noLink = true;
1198        } else {
1199            // add file icons
1200            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1201            $link['class'] .= ' mediafile mf_'.$class;
1202            $link['url'] = ml(
1203                $src,
1204                array(
1205                    'id' => $ID,
1206                    'cache' => $cache,
1207                    'rev' => $this->_getLastMediaRevisionAt($src)
1208                ),
1209                true
1210            );
1211            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
1212        }
1213
1214        if (!empty($hash)) $link['url'] .= '#'.$hash;
1215
1216        //markup non existing files
1217        if(!$exists) {
1218            $link['class'] .= ' wikilink2';
1219        }
1220
1221        //output formatted
1222        if($return) {
1223            if($linking == 'nolink' || $noLink) return $link['name'];
1224            else return $this->_formatLink($link);
1225        } else {
1226            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1227            else $this->doc .= $this->_formatLink($link);
1228        }
1229    }
1230
1231    /**
1232     * Render an external media file
1233     *
1234     * @param string $src     full media URL
1235     * @param string $title   descriptive text
1236     * @param string $align   left|center|right
1237     * @param int    $width   width of media in pixel
1238     * @param int    $height  height of media in pixel
1239     * @param string $cache   cache|recache|nocache
1240     * @param string $linking linkonly|detail|nolink
1241     * @param bool   $return  return HTML instead of adding to $doc
1242     * @return void|string writes to doc attribute or returns html depends on $return
1243     */
1244    function externalmedia($src, $title = null, $align = null, $width = null,
1245                           $height = null, $cache = null, $linking = null, $return = false) {
1246        if(link_isinterwiki($src)){
1247            list($shortcut, $reference) = explode('>', $src, 2);
1248            $exists = null;
1249            $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
1250        }
1251        list($src, $hash) = explode('#', $src, 2);
1252        $noLink = false;
1253        $render = ($linking == 'linkonly') ? false : true;
1254        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1255
1256        $link['url'] = ml($src, array('cache' => $cache));
1257
1258        list($ext, $mime) = mimetype($src, false);
1259        if(substr($mime, 0, 5) == 'image' && $render) {
1260            // link only jpeg images
1261            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
1262        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1263            // don't link movies
1264            $noLink = true;
1265        } else {
1266            // add file icons
1267            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1268            $link['class'] .= ' mediafile mf_'.$class;
1269        }
1270
1271        if($hash) $link['url'] .= '#'.$hash;
1272
1273        //output formatted
1274        if($return) {
1275            if($linking == 'nolink' || $noLink) return $link['name'];
1276            else return $this->_formatLink($link);
1277        } else {
1278            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1279            else $this->doc .= $this->_formatLink($link);
1280        }
1281    }
1282
1283    /**
1284     * Renders an RSS feed
1285     *
1286     * @param string $url    URL of the feed
1287     * @param array  $params Finetuning of the output
1288     *
1289     * @author Andreas Gohr <andi@splitbrain.org>
1290     */
1291    function rss($url, $params) {
1292        global $lang;
1293        global $conf;
1294
1295        require_once(DOKU_INC.'inc/FeedParser.php');
1296        $feed = new FeedParser();
1297        $feed->set_feed_url($url);
1298
1299        //disable warning while fetching
1300        if(!defined('DOKU_E_LEVEL')) {
1301            $elvl = error_reporting(E_ERROR);
1302        }
1303        $rc = $feed->init();
1304        if(isset($elvl)) {
1305            error_reporting($elvl);
1306        }
1307
1308        if($params['nosort']) $feed->enable_order_by_date(false);
1309
1310        //decide on start and end
1311        if($params['reverse']) {
1312            $mod   = -1;
1313            $start = $feed->get_item_quantity() - 1;
1314            $end   = $start - ($params['max']);
1315            $end   = ($end < -1) ? -1 : $end;
1316        } else {
1317            $mod   = 1;
1318            $start = 0;
1319            $end   = $feed->get_item_quantity();
1320            $end   = ($end > $params['max']) ? $params['max'] : $end;
1321        }
1322
1323        $this->doc .= '<ul class="rss">';
1324        if($rc) {
1325            for($x = $start; $x != $end; $x += $mod) {
1326                $item = $feed->get_item($x);
1327                $this->doc .= '<li><div class="li">';
1328                // support feeds without links
1329                $lnkurl = $item->get_permalink();
1330                if($lnkurl) {
1331                    // title is escaped by SimplePie, we unescape here because it
1332                    // is escaped again in externallink() FS#1705
1333                    $this->externallink(
1334                        $item->get_permalink(),
1335                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
1336                    );
1337                } else {
1338                    $this->doc .= ' '.$item->get_title();
1339                }
1340                if($params['author']) {
1341                    $author = $item->get_author(0);
1342                    if($author) {
1343                        $name = $author->get_name();
1344                        if(!$name) $name = $author->get_email();
1345                        if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name);
1346                    }
1347                }
1348                if($params['date']) {
1349                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1350                }
1351                if($params['details']) {
1352                    $this->doc .= '<div class="detail">';
1353                    if($conf['htmlok']) {
1354                        $this->doc .= $item->get_description();
1355                    } else {
1356                        $this->doc .= strip_tags($item->get_description());
1357                    }
1358                    $this->doc .= '</div>';
1359                }
1360
1361                $this->doc .= '</div></li>';
1362            }
1363        } else {
1364            $this->doc .= '<li><div class="li">';
1365            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1366            $this->externallink($url);
1367            if($conf['allowdebug']) {
1368                $this->doc .= '<!--'.hsc($feed->error).'-->';
1369            }
1370            $this->doc .= '</div></li>';
1371        }
1372        $this->doc .= '</ul>';
1373    }
1374
1375    /**
1376     * Start a table
1377     *
1378     * @param int $maxcols maximum number of columns
1379     * @param int $numrows NOT IMPLEMENTED
1380     * @param int $pos byte position in the original source
1381     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1382     */
1383    function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
1384        // initialize the row counter used for classes
1385        $this->_counter['row_counter'] = 0;
1386        $class                         = 'table';
1387        if($classes !== null) {
1388            if(is_array($classes)) $classes = join(' ', $classes);
1389            $class .= ' ' . $classes;
1390        }
1391        if($pos !== null) {
1392            $hid = $this->_headerToLink($class, true);
1393            $data = array();
1394            $data['target'] = 'table';
1395            $data['name'] = '';
1396            $data['hid'] = $hid;
1397            $class .= ' '.$this->startSectionEdit($pos, $data);
1398        }
1399        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1400            DOKU_LF;
1401    }
1402
1403    /**
1404     * Close a table
1405     *
1406     * @param int $pos byte position in the original source
1407     */
1408    function table_close($pos = null) {
1409        $this->doc .= '</table></div>'.DOKU_LF;
1410        if($pos !== null) {
1411            $this->finishSectionEdit($pos);
1412        }
1413    }
1414
1415    /**
1416     * Open a table header
1417     */
1418    function tablethead_open() {
1419        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1420    }
1421
1422    /**
1423     * Close a table header
1424     */
1425    function tablethead_close() {
1426        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1427    }
1428
1429    /**
1430     * Open a table body
1431     */
1432    function tabletbody_open() {
1433        $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
1434    }
1435
1436    /**
1437     * Close a table body
1438     */
1439    function tabletbody_close() {
1440        $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
1441    }
1442
1443    /**
1444     * Open a table footer
1445     */
1446    function tabletfoot_open() {
1447        $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
1448    }
1449
1450    /**
1451     * Close a table footer
1452     */
1453    function tabletfoot_close() {
1454        $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
1455    }
1456
1457    /**
1458     * Open a table row
1459     *
1460     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1461     */
1462    function tablerow_open($classes = null) {
1463        // initialize the cell counter used for classes
1464        $this->_counter['cell_counter'] = 0;
1465        $class                          = 'row'.$this->_counter['row_counter']++;
1466        if($classes !== null) {
1467            if(is_array($classes)) $classes = join(' ', $classes);
1468            $class .= ' ' . $classes;
1469        }
1470        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1471    }
1472
1473    /**
1474     * Close a table row
1475     */
1476    function tablerow_close() {
1477        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1478    }
1479
1480    /**
1481     * Open a table header cell
1482     *
1483     * @param int    $colspan
1484     * @param string $align left|center|right
1485     * @param int    $rowspan
1486     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1487     */
1488    function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1489        $class = 'class="col'.$this->_counter['cell_counter']++;
1490        if(!is_null($align)) {
1491            $class .= ' '.$align.'align';
1492        }
1493        if($classes !== null) {
1494            if(is_array($classes)) $classes = join(' ', $classes);
1495            $class .= ' ' . $classes;
1496        }
1497        $class .= '"';
1498        $this->doc .= '<th '.$class;
1499        if($colspan > 1) {
1500            $this->_counter['cell_counter'] += $colspan - 1;
1501            $this->doc .= ' colspan="'.$colspan.'"';
1502        }
1503        if($rowspan > 1) {
1504            $this->doc .= ' rowspan="'.$rowspan.'"';
1505        }
1506        $this->doc .= '>';
1507    }
1508
1509    /**
1510     * Close a table header cell
1511     */
1512    function tableheader_close() {
1513        $this->doc .= '</th>';
1514    }
1515
1516    /**
1517     * Open a table cell
1518     *
1519     * @param int       $colspan
1520     * @param string    $align left|center|right
1521     * @param int       $rowspan
1522     * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
1523     */
1524    function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1525        $class = 'class="col'.$this->_counter['cell_counter']++;
1526        if(!is_null($align)) {
1527            $class .= ' '.$align.'align';
1528        }
1529        if($classes !== null) {
1530            if(is_array($classes)) $classes = join(' ', $classes);
1531            $class .= ' ' . $classes;
1532        }
1533        $class .= '"';
1534        $this->doc .= '<td '.$class;
1535        if($colspan > 1) {
1536            $this->_counter['cell_counter'] += $colspan - 1;
1537            $this->doc .= ' colspan="'.$colspan.'"';
1538        }
1539        if($rowspan > 1) {
1540            $this->doc .= ' rowspan="'.$rowspan.'"';
1541        }
1542        $this->doc .= '>';
1543    }
1544
1545    /**
1546     * Close a table cell
1547     */
1548    function tablecell_close() {
1549        $this->doc .= '</td>';
1550    }
1551
1552    /**
1553     * Returns the current header level.
1554     * (required e.g. by the filelist plugin)
1555     *
1556     * @return int The current header level
1557     */
1558    function getLastlevel() {
1559        return $this->lastlevel;
1560    }
1561
1562    #region Utility functions
1563
1564    /**
1565     * Build a link
1566     *
1567     * Assembles all parts defined in $link returns HTML for the link
1568     *
1569     * @param array $link attributes of a link
1570     * @return string
1571     *
1572     * @author Andreas Gohr <andi@splitbrain.org>
1573     */
1574    function _formatLink($link) {
1575        //make sure the url is XHTML compliant (skip mailto)
1576        if(substr($link['url'], 0, 7) != 'mailto:') {
1577            $link['url'] = str_replace('&', '&amp;', $link['url']);
1578            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1579        }
1580        //remove double encodings in titles
1581        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1582
1583        // be sure there are no bad chars in url or title
1584        // (we can't do this for name because it can contain an img tag)
1585        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1586        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1587
1588        $ret = '';
1589        $ret .= $link['pre'];
1590        $ret .= '<a href="'.$link['url'].'"';
1591        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1592        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1593        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1594        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1595        if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
1596        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1597        $ret .= '>';
1598        $ret .= $link['name'];
1599        $ret .= '</a>';
1600        $ret .= $link['suf'];
1601        return $ret;
1602    }
1603
1604    /**
1605     * Renders internal and external media
1606     *
1607     * @author Andreas Gohr <andi@splitbrain.org>
1608     * @param string $src       media ID
1609     * @param string $title     descriptive text
1610     * @param string $align     left|center|right
1611     * @param int    $width     width of media in pixel
1612     * @param int    $height    height of media in pixel
1613     * @param string $cache     cache|recache|nocache
1614     * @param bool   $render    should the media be embedded inline or just linked
1615     * @return string
1616     */
1617    function _media($src, $title = null, $align = null, $width = null,
1618                    $height = null, $cache = null, $render = true) {
1619
1620        $ret = '';
1621
1622        list($ext, $mime) = mimetype($src);
1623        if(substr($mime, 0, 5) == 'image') {
1624            // first get the $title
1625            if(!is_null($title)) {
1626                $title = $this->_xmlEntities($title);
1627            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1628                //try to use the caption from IPTC/EXIF
1629                require_once(DOKU_INC.'inc/JpegMeta.php');
1630                $jpeg = new JpegMeta(mediaFN($src));
1631                if($jpeg !== false) $cap = $jpeg->getTitle();
1632                if(!empty($cap)) {
1633                    $title = $this->_xmlEntities($cap);
1634                }
1635            }
1636            if(!$render) {
1637                // if the picture is not supposed to be rendered
1638                // return the title of the picture
1639                if(!$title) {
1640                    // just show the sourcename
1641                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1642                }
1643                return $title;
1644            }
1645            //add image tag
1646            $ret .= '<img src="' . ml(
1647                    $src,
1648                    array(
1649                        'w' => $width, 'h' => $height,
1650                        'cache' => $cache,
1651                        'rev' => $this->_getLastMediaRevisionAt($src)
1652                    )
1653                ) . '"';
1654            $ret .= ' class="media'.$align.'"';
1655
1656            if($title) {
1657                $ret .= ' title="'.$title.'"';
1658                $ret .= ' alt="'.$title.'"';
1659            } else {
1660                $ret .= ' alt=""';
1661            }
1662
1663            if(!is_null($width))
1664                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1665
1666            if(!is_null($height))
1667                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1668
1669            $ret .= ' />';
1670
1671        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1672            // first get the $title
1673            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1674            if(!$render) {
1675                // if the file is not supposed to be rendered
1676                // return the title of the file (just the sourcename if there is no title)
1677                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1678            }
1679
1680            $att          = array();
1681            $att['class'] = "media$align";
1682            if($title) {
1683                $att['title'] = $title;
1684            }
1685
1686            if(media_supportedav($mime, 'video')) {
1687                //add video
1688                $ret .= $this->_video($src, $width, $height, $att);
1689            }
1690            if(media_supportedav($mime, 'audio')) {
1691                //add audio
1692                $ret .= $this->_audio($src, $att);
1693            }
1694
1695        } elseif($mime == 'application/x-shockwave-flash') {
1696            if(!$render) {
1697                // if the flash is not supposed to be rendered
1698                // return the title of the flash
1699                if(!$title) {
1700                    // just show the sourcename
1701                    $title = utf8_basename(noNS($src));
1702                }
1703                return $this->_xmlEntities($title);
1704            }
1705
1706            $att          = array();
1707            $att['class'] = "media$align";
1708            if($align == 'right') $att['align'] = 'right';
1709            if($align == 'left') $att['align'] = 'left';
1710            $ret .= html_flashobject(
1711                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1712                array('quality' => 'high'),
1713                null,
1714                $att,
1715                $this->_xmlEntities($title)
1716            );
1717        } elseif($title) {
1718            // well at least we have a title to display
1719            $ret .= $this->_xmlEntities($title);
1720        } else {
1721            // just show the sourcename
1722            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1723        }
1724
1725        return $ret;
1726    }
1727
1728    /**
1729     * Escape string for output
1730     *
1731     * @param $string
1732     * @return string
1733     */
1734    function _xmlEntities($string) {
1735        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1736    }
1737
1738    /**
1739     * Creates a linkid from a headline
1740     *
1741     * @author Andreas Gohr <andi@splitbrain.org>
1742     * @param string  $title   The headline title
1743     * @param boolean $create  Create a new unique ID?
1744     * @return string
1745     */
1746    function _headerToLink($title, $create = false) {
1747        if($create) {
1748            return sectionID($title, $this->headers);
1749        } else {
1750            $check = false;
1751            return sectionID($title, $check);
1752        }
1753    }
1754
1755    /**
1756     * Construct a title and handle images in titles
1757     *
1758     * @author Harry Fuecks <hfuecks@gmail.com>
1759     * @param string|array $title    either string title or media array
1760     * @param string       $default  default title if nothing else is found
1761     * @param bool         $isImage  will be set to true if it's a media file
1762     * @param null|string  $id       linked page id (used to extract title from first heading)
1763     * @param string       $linktype content|navigation
1764     * @return string      HTML of the title, might be full image tag or just escaped text
1765     */
1766    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1767        $isImage = false;
1768        if(is_array($title)) {
1769            $isImage = true;
1770            return $this->_imageTitle($title);
1771        } elseif(is_null($title) || trim($title) == '') {
1772            if(useHeading($linktype) && $id) {
1773                $heading = p_get_first_heading($id);
1774                if(!blank($heading)) {
1775                    return $this->_xmlEntities($heading);
1776                }
1777            }
1778            return $this->_xmlEntities($default);
1779        } else {
1780            return $this->_xmlEntities($title);
1781        }
1782    }
1783
1784    /**
1785     * Returns HTML code for images used in link titles
1786     *
1787     * @author Andreas Gohr <andi@splitbrain.org>
1788     * @param array $img
1789     * @return string HTML img tag or similar
1790     */
1791    function _imageTitle($img) {
1792        global $ID;
1793
1794        // some fixes on $img['src']
1795        // see internalmedia() and externalmedia()
1796        list($img['src']) = explode('#', $img['src'], 2);
1797        if($img['type'] == 'internalmedia') {
1798            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
1799        }
1800
1801        return $this->_media(
1802            $img['src'],
1803            $img['title'],
1804            $img['align'],
1805            $img['width'],
1806            $img['height'],
1807            $img['cache']
1808        );
1809    }
1810
1811    /**
1812     * helperfunction to return a basic link to a media
1813     *
1814     * used in internalmedia() and externalmedia()
1815     *
1816     * @author   Pierre Spring <pierre.spring@liip.ch>
1817     * @param string $src       media ID
1818     * @param string $title     descriptive text
1819     * @param string $align     left|center|right
1820     * @param int    $width     width of media in pixel
1821     * @param int    $height    height of media in pixel
1822     * @param string $cache     cache|recache|nocache
1823     * @param bool   $render    should the media be embedded inline or just linked
1824     * @return array associative array with link config
1825     */
1826    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1827        global $conf;
1828
1829        $link           = array();
1830        $link['class']  = 'media';
1831        $link['style']  = '';
1832        $link['pre']    = '';
1833        $link['suf']    = '';
1834        $link['more']   = '';
1835        $link['target'] = $conf['target']['media'];
1836        if($conf['target']['media']) $link['rel'] = 'noopener';
1837        $link['title']  = $this->_xmlEntities($src);
1838        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1839
1840        return $link;
1841    }
1842
1843    /**
1844     * Embed video(s) in HTML
1845     *
1846     * @author Anika Henke <anika@selfthinker.org>
1847     * @author Schplurtz le Déboulonné <Schplurtz@laposte.net>
1848     *
1849     * @param string $src         - ID of video to embed
1850     * @param int    $width       - width of the video in pixels
1851     * @param int    $height      - height of the video in pixels
1852     * @param array  $atts        - additional attributes for the <video> tag
1853     * @return string
1854     */
1855    function _video($src, $width, $height, $atts = null) {
1856        // prepare width and height
1857        if(is_null($atts)) $atts = array();
1858        $atts['width']  = (int) $width;
1859        $atts['height'] = (int) $height;
1860        if(!$atts['width']) $atts['width'] = 320;
1861        if(!$atts['height']) $atts['height'] = 240;
1862
1863        $posterUrl = '';
1864        $files = array();
1865        $tracks = array();
1866        $isExternal = media_isexternal($src);
1867
1868        if ($isExternal) {
1869            // take direct source for external files
1870            list(/*ext*/, $srcMime) = mimetype($src);
1871            $files[$srcMime] = $src;
1872        } else {
1873            // prepare alternative formats
1874            $extensions   = array('webm', 'ogv', 'mp4');
1875            $files        = media_alternativefiles($src, $extensions);
1876            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1877            $tracks       = media_trackfiles($src);
1878            if(!empty($poster)) {
1879                $posterUrl = ml(reset($poster), '', true, '&');
1880            }
1881        }
1882
1883        $out = '';
1884        // open video tag
1885        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1886        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1887        $out .= '>'.NL;
1888        $fallback = '';
1889
1890        // output source for each alternative video format
1891        foreach($files as $mime => $file) {
1892            if ($isExternal) {
1893                $url = $file;
1894                $linkType = 'externalmedia';
1895            } else {
1896                $url = ml($file, '', true, '&');
1897                $linkType = 'internalmedia';
1898            }
1899            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1900
1901            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1902            // alternative content (just a link to the file)
1903            $fallback .= $this->$linkType(
1904                $file,
1905                $title,
1906                null,
1907                null,
1908                null,
1909                $cache = null,
1910                $linking = 'linkonly',
1911                $return = true
1912            );
1913        }
1914
1915        // output each track if any
1916        foreach( $tracks as $trackid => $info ) {
1917            list( $kind, $srclang ) = array_map( 'hsc', $info );
1918            $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
1919            $out .= "label=\"$srclang\" ";
1920            $out .= 'src="'.ml($trackid, '', true).'">'.NL;
1921        }
1922
1923        // finish
1924        $out .= $fallback;
1925        $out .= '</video>'.NL;
1926        return $out;
1927    }
1928
1929    /**
1930     * Embed audio in HTML
1931     *
1932     * @author Anika Henke <anika@selfthinker.org>
1933     *
1934     * @param string $src       - ID of audio to embed
1935     * @param array  $atts      - additional attributes for the <audio> tag
1936     * @return string
1937     */
1938    function _audio($src, $atts = array()) {
1939        $files = array();
1940        $isExternal = media_isexternal($src);
1941
1942        if ($isExternal) {
1943            // take direct source for external files
1944            list(/*ext*/, $srcMime) = mimetype($src);
1945            $files[$srcMime] = $src;
1946        } else {
1947            // prepare alternative formats
1948            $extensions   = array('ogg', 'mp3', 'wav');
1949            $files        = media_alternativefiles($src, $extensions);
1950        }
1951
1952        $out = '';
1953        // open audio tag
1954        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1955        $fallback = '';
1956
1957        // output source for each alternative audio format
1958        foreach($files as $mime => $file) {
1959            if ($isExternal) {
1960                $url = $file;
1961                $linkType = 'externalmedia';
1962            } else {
1963                $url = ml($file, '', true, '&');
1964                $linkType = 'internalmedia';
1965            }
1966            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1967
1968            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1969            // alternative content (just a link to the file)
1970            $fallback .= $this->$linkType(
1971                $file,
1972                $title,
1973                null,
1974                null,
1975                null,
1976                $cache = null,
1977                $linking = 'linkonly',
1978                $return = true
1979            );
1980        }
1981
1982        // finish
1983        $out .= $fallback;
1984        $out .= '</audio>'.NL;
1985        return $out;
1986    }
1987
1988    /**
1989     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1990     * which returns an existing media revision less or equal to rev or date_at
1991     *
1992     * @author lisps
1993     * @param string $media_id
1994     * @access protected
1995     * @return string revision ('' for current)
1996     */
1997    function _getLastMediaRevisionAt($media_id){
1998        if(!$this->date_at || media_isexternal($media_id)) return '';
1999        $pagelog = new MediaChangeLog($media_id);
2000        return $pagelog->getLastRevisionAt($this->date_at);
2001    }
2002
2003    #endregion
2004}
2005
2006//Setup VIM: ex: et ts=4 :
2007