xref: /dokuwiki/inc/parser/xhtml.php (revision 63da9d57cb077059cb2eedc5d3646638a015d150)
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['at'] = $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            $class .= ' '.$this->startSectionEdit($pos, 'table');
1343        }
1344        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1345            DOKU_LF;
1346    }
1347
1348    /**
1349     * Close a table
1350     *
1351     * @param int $pos byte position in the original source
1352     */
1353    function table_close($pos = null) {
1354        $this->doc .= '</table></div>'.DOKU_LF;
1355        if($pos !== null) {
1356            $this->finishSectionEdit($pos);
1357        }
1358    }
1359
1360    /**
1361     * Open a table header
1362     */
1363    function tablethead_open() {
1364        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1365    }
1366
1367    /**
1368     * Close a table header
1369     */
1370    function tablethead_close() {
1371        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1372    }
1373
1374    /**
1375     * Open a table body
1376     */
1377    function tabletbody_open() {
1378        $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
1379    }
1380
1381    /**
1382     * Close a table body
1383     */
1384    function tabletbody_close() {
1385        $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
1386    }
1387
1388    /**
1389     * Open a table footer
1390     */
1391    function tabletfoot_open() {
1392        $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
1393    }
1394
1395    /**
1396     * Close a table footer
1397     */
1398    function tabletfoot_close() {
1399        $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
1400    }
1401
1402    /**
1403     * Open a table row
1404     *
1405     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1406     */
1407    function tablerow_open($classes = null) {
1408        // initialize the cell counter used for classes
1409        $this->_counter['cell_counter'] = 0;
1410        $class                          = 'row'.$this->_counter['row_counter']++;
1411        if($classes !== null) {
1412            if(is_array($classes)) $classes = join(' ', $classes);
1413            $class .= ' ' . $classes;
1414        }
1415        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1416    }
1417
1418    /**
1419     * Close a table row
1420     */
1421    function tablerow_close() {
1422        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1423    }
1424
1425    /**
1426     * Open a table header cell
1427     *
1428     * @param int    $colspan
1429     * @param string $align left|center|right
1430     * @param int    $rowspan
1431     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1432     */
1433    function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1434        $class = 'class="col'.$this->_counter['cell_counter']++;
1435        if(!is_null($align)) {
1436            $class .= ' '.$align.'align';
1437        }
1438        if($classes !== null) {
1439            if(is_array($classes)) $classes = join(' ', $classes);
1440            $class .= ' ' . $classes;
1441        }
1442        $class .= '"';
1443        $this->doc .= '<th '.$class;
1444        if($colspan > 1) {
1445            $this->_counter['cell_counter'] += $colspan - 1;
1446            $this->doc .= ' colspan="'.$colspan.'"';
1447        }
1448        if($rowspan > 1) {
1449            $this->doc .= ' rowspan="'.$rowspan.'"';
1450        }
1451        $this->doc .= '>';
1452    }
1453
1454    /**
1455     * Close a table header cell
1456     */
1457    function tableheader_close() {
1458        $this->doc .= '</th>';
1459    }
1460
1461    /**
1462     * Open a table cell
1463     *
1464     * @param int       $colspan
1465     * @param string    $align left|center|right
1466     * @param int       $rowspan
1467     * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
1468     */
1469    function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1470        $class = 'class="col'.$this->_counter['cell_counter']++;
1471        if(!is_null($align)) {
1472            $class .= ' '.$align.'align';
1473        }
1474        if($classes !== null) {
1475            if(is_array($classes)) $classes = join(' ', $classes);
1476            $class .= ' ' . $classes;
1477        }
1478        $class .= '"';
1479        $this->doc .= '<td '.$class;
1480        if($colspan > 1) {
1481            $this->_counter['cell_counter'] += $colspan - 1;
1482            $this->doc .= ' colspan="'.$colspan.'"';
1483        }
1484        if($rowspan > 1) {
1485            $this->doc .= ' rowspan="'.$rowspan.'"';
1486        }
1487        $this->doc .= '>';
1488    }
1489
1490    /**
1491     * Close a table cell
1492     */
1493    function tablecell_close() {
1494        $this->doc .= '</td>';
1495    }
1496
1497    /**
1498     * Returns the current header level.
1499     * (required e.g. by the filelist plugin)
1500     *
1501     * @return int The current header level
1502     */
1503    function getLastlevel() {
1504        return $this->lastlevel;
1505    }
1506
1507    #region Utility functions
1508
1509    /**
1510     * Build a link
1511     *
1512     * Assembles all parts defined in $link returns HTML for the link
1513     *
1514     * @param array $link attributes of a link
1515     * @return string
1516     *
1517     * @author Andreas Gohr <andi@splitbrain.org>
1518     */
1519    function _formatLink($link) {
1520        //make sure the url is XHTML compliant (skip mailto)
1521        if(substr($link['url'], 0, 7) != 'mailto:') {
1522            $link['url'] = str_replace('&', '&amp;', $link['url']);
1523            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1524        }
1525        //remove double encodings in titles
1526        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1527
1528        // be sure there are no bad chars in url or title
1529        // (we can't do this for name because it can contain an img tag)
1530        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1531        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1532
1533        $ret = '';
1534        $ret .= $link['pre'];
1535        $ret .= '<a href="'.$link['url'].'"';
1536        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1537        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1538        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1539        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1540        if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
1541        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1542        $ret .= '>';
1543        $ret .= $link['name'];
1544        $ret .= '</a>';
1545        $ret .= $link['suf'];
1546        return $ret;
1547    }
1548
1549    /**
1550     * Renders internal and external media
1551     *
1552     * @author Andreas Gohr <andi@splitbrain.org>
1553     * @param string $src       media ID
1554     * @param string $title     descriptive text
1555     * @param string $align     left|center|right
1556     * @param int    $width     width of media in pixel
1557     * @param int    $height    height of media in pixel
1558     * @param string $cache     cache|recache|nocache
1559     * @param bool   $render    should the media be embedded inline or just linked
1560     * @return string
1561     */
1562    function _media($src, $title = null, $align = null, $width = null,
1563                    $height = null, $cache = null, $render = true) {
1564
1565        $ret = '';
1566
1567        list($ext, $mime) = mimetype($src);
1568        if(substr($mime, 0, 5) == 'image') {
1569            // first get the $title
1570            if(!is_null($title)) {
1571                $title = $this->_xmlEntities($title);
1572            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1573                //try to use the caption from IPTC/EXIF
1574                require_once(DOKU_INC.'inc/JpegMeta.php');
1575                $jpeg = new JpegMeta(mediaFN($src));
1576                if($jpeg !== false) $cap = $jpeg->getTitle();
1577                if(!empty($cap)) {
1578                    $title = $this->_xmlEntities($cap);
1579                }
1580            }
1581            if(!$render) {
1582                // if the picture is not supposed to be rendered
1583                // return the title of the picture
1584                if(!$title) {
1585                    // just show the sourcename
1586                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1587                }
1588                return $title;
1589            }
1590            //add image tag
1591            $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"';
1592            $ret .= ' class="media'.$align.'"';
1593
1594            if($title) {
1595                $ret .= ' title="'.$title.'"';
1596                $ret .= ' alt="'.$title.'"';
1597            } else {
1598                $ret .= ' alt=""';
1599            }
1600
1601            if(!is_null($width))
1602                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1603
1604            if(!is_null($height))
1605                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1606
1607            $ret .= ' />';
1608
1609        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1610            // first get the $title
1611            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1612            if(!$render) {
1613                // if the file is not supposed to be rendered
1614                // return the title of the file (just the sourcename if there is no title)
1615                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1616            }
1617
1618            $att          = array();
1619            $att['class'] = "media$align";
1620            if($title) {
1621                $att['title'] = $title;
1622            }
1623
1624            if(media_supportedav($mime, 'video')) {
1625                //add video
1626                $ret .= $this->_video($src, $width, $height, $att);
1627            }
1628            if(media_supportedav($mime, 'audio')) {
1629                //add audio
1630                $ret .= $this->_audio($src, $att);
1631            }
1632
1633        } elseif($mime == 'application/x-shockwave-flash') {
1634            if(!$render) {
1635                // if the flash is not supposed to be rendered
1636                // return the title of the flash
1637                if(!$title) {
1638                    // just show the sourcename
1639                    $title = utf8_basename(noNS($src));
1640                }
1641                return $this->_xmlEntities($title);
1642            }
1643
1644            $att          = array();
1645            $att['class'] = "media$align";
1646            if($align == 'right') $att['align'] = 'right';
1647            if($align == 'left') $att['align'] = 'left';
1648            $ret .= html_flashobject(
1649                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1650                array('quality' => 'high'),
1651                null,
1652                $att,
1653                $this->_xmlEntities($title)
1654            );
1655        } elseif($title) {
1656            // well at least we have a title to display
1657            $ret .= $this->_xmlEntities($title);
1658        } else {
1659            // just show the sourcename
1660            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1661        }
1662
1663        return $ret;
1664    }
1665
1666    /**
1667     * Escape string for output
1668     *
1669     * @param $string
1670     * @return string
1671     */
1672    function _xmlEntities($string) {
1673        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1674    }
1675
1676    /**
1677     * Creates a linkid from a headline
1678     *
1679     * @author Andreas Gohr <andi@splitbrain.org>
1680     * @param string  $title   The headline title
1681     * @param boolean $create  Create a new unique ID?
1682     * @return string
1683     */
1684    function _headerToLink($title, $create = false) {
1685        if($create) {
1686            return sectionID($title, $this->headers);
1687        } else {
1688            $check = false;
1689            return sectionID($title, $check);
1690        }
1691    }
1692
1693    /**
1694     * Construct a title and handle images in titles
1695     *
1696     * @author Harry Fuecks <hfuecks@gmail.com>
1697     * @param string|array $title    either string title or media array
1698     * @param string       $default  default title if nothing else is found
1699     * @param bool         $isImage  will be set to true if it's a media file
1700     * @param null|string  $id       linked page id (used to extract title from first heading)
1701     * @param string       $linktype content|navigation
1702     * @return string      HTML of the title, might be full image tag or just escaped text
1703     */
1704    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1705        $isImage = false;
1706        if(is_array($title)) {
1707            $isImage = true;
1708            return $this->_imageTitle($title);
1709        } elseif(is_null($title) || trim($title) == '') {
1710            if(useHeading($linktype) && $id) {
1711                $heading = p_get_first_heading($id);
1712                if(!blank($heading)) {
1713                    return $this->_xmlEntities($heading);
1714                }
1715            }
1716            return $this->_xmlEntities($default);
1717        } else {
1718            return $this->_xmlEntities($title);
1719        }
1720    }
1721
1722    /**
1723     * Returns HTML code for images used in link titles
1724     *
1725     * @author Andreas Gohr <andi@splitbrain.org>
1726     * @param array $img
1727     * @return string HTML img tag or similar
1728     */
1729    function _imageTitle($img) {
1730        global $ID;
1731
1732        // some fixes on $img['src']
1733        // see internalmedia() and externalmedia()
1734        list($img['src']) = explode('#', $img['src'], 2);
1735        if($img['type'] == 'internalmedia') {
1736            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
1737        }
1738
1739        return $this->_media(
1740            $img['src'],
1741            $img['title'],
1742            $img['align'],
1743            $img['width'],
1744            $img['height'],
1745            $img['cache']
1746        );
1747    }
1748
1749    /**
1750     * helperfunction to return a basic link to a media
1751     *
1752     * used in internalmedia() and externalmedia()
1753     *
1754     * @author   Pierre Spring <pierre.spring@liip.ch>
1755     * @param string $src       media ID
1756     * @param string $title     descriptive text
1757     * @param string $align     left|center|right
1758     * @param int    $width     width of media in pixel
1759     * @param int    $height    height of media in pixel
1760     * @param string $cache     cache|recache|nocache
1761     * @param bool   $render    should the media be embedded inline or just linked
1762     * @return array associative array with link config
1763     */
1764    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1765        global $conf;
1766
1767        $link           = array();
1768        $link['class']  = 'media';
1769        $link['style']  = '';
1770        $link['pre']    = '';
1771        $link['suf']    = '';
1772        $link['more']   = '';
1773        $link['target'] = $conf['target']['media'];
1774        if($conf['target']['media']) $link['rel'] = 'noopener';
1775        $link['title']  = $this->_xmlEntities($src);
1776        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1777
1778        return $link;
1779    }
1780
1781    /**
1782     * Embed video(s) in HTML
1783     *
1784     * @author Anika Henke <anika@selfthinker.org>
1785     *
1786     * @param string $src         - ID of video to embed
1787     * @param int    $width       - width of the video in pixels
1788     * @param int    $height      - height of the video in pixels
1789     * @param array  $atts        - additional attributes for the <video> tag
1790     * @return string
1791     */
1792    function _video($src, $width, $height, $atts = null) {
1793        // prepare width and height
1794        if(is_null($atts)) $atts = array();
1795        $atts['width']  = (int) $width;
1796        $atts['height'] = (int) $height;
1797        if(!$atts['width']) $atts['width'] = 320;
1798        if(!$atts['height']) $atts['height'] = 240;
1799
1800        $posterUrl = '';
1801        $files = array();
1802        $isExternal = media_isexternal($src);
1803
1804        if ($isExternal) {
1805            // take direct source for external files
1806            list(/*ext*/, $srcMime) = mimetype($src);
1807            $files[$srcMime] = $src;
1808        } else {
1809            // prepare alternative formats
1810            $extensions   = array('webm', 'ogv', 'mp4');
1811            $files        = media_alternativefiles($src, $extensions);
1812            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1813            if(!empty($poster)) {
1814                $posterUrl = ml(reset($poster), '', true, '&');
1815            }
1816        }
1817
1818        $out = '';
1819        // open video tag
1820        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1821        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1822        $out .= '>'.NL;
1823        $fallback = '';
1824
1825        // output source for each alternative video format
1826        foreach($files as $mime => $file) {
1827            if ($isExternal) {
1828                $url = $file;
1829                $linkType = 'externalmedia';
1830            } else {
1831                $url = ml($file, '', true, '&');
1832                $linkType = 'internalmedia';
1833            }
1834            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1835
1836            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1837            // alternative content (just a link to the file)
1838            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1839        }
1840
1841        // finish
1842        $out .= $fallback;
1843        $out .= '</video>'.NL;
1844        return $out;
1845    }
1846
1847    /**
1848     * Embed audio in HTML
1849     *
1850     * @author Anika Henke <anika@selfthinker.org>
1851     *
1852     * @param string $src       - ID of audio to embed
1853     * @param array  $atts      - additional attributes for the <audio> tag
1854     * @return string
1855     */
1856    function _audio($src, $atts = array()) {
1857        $files = array();
1858        $isExternal = media_isexternal($src);
1859
1860        if ($isExternal) {
1861            // take direct source for external files
1862            list(/*ext*/, $srcMime) = mimetype($src);
1863            $files[$srcMime] = $src;
1864        } else {
1865            // prepare alternative formats
1866            $extensions   = array('ogg', 'mp3', 'wav');
1867            $files        = media_alternativefiles($src, $extensions);
1868        }
1869
1870        $out = '';
1871        // open audio tag
1872        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1873        $fallback = '';
1874
1875        // output source for each alternative audio format
1876        foreach($files as $mime => $file) {
1877            if ($isExternal) {
1878                $url = $file;
1879                $linkType = 'externalmedia';
1880            } else {
1881                $url = ml($file, '', true, '&');
1882                $linkType = 'internalmedia';
1883            }
1884            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1885
1886            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1887            // alternative content (just a link to the file)
1888            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1889        }
1890
1891        // finish
1892        $out .= $fallback;
1893        $out .= '</audio>'.NL;
1894        return $out;
1895    }
1896
1897    /**
1898     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1899     * which returns an existing media revision less or equal to rev or date_at
1900     *
1901     * @author lisps
1902     * @param string $media_id
1903     * @access protected
1904     * @return string revision ('' for current)
1905     */
1906    function _getLastMediaRevisionAt($media_id){
1907        if(!$this->date_at || media_isexternal($media_id)) return '';
1908        $pagelog = new MediaChangeLog($media_id);
1909        return $pagelog->getLastRevisionAt($this->date_at);
1910    }
1911
1912    #endregion
1913}
1914
1915//Setup VIM: ex: et ts=4 :
1916