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