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