xref: /dokuwiki/inc/parser/xhtml.php (revision 7c6a857aa28f6f8ef108b04eadb2b7a01a672c62)
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        if($filename) {
637            // add icon
638            list($ext) = mimetype($filename, false);
639            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
640            $class = 'mediafile mf_'.$class;
641
642            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
643            $this->doc .= '<dt><a href="'.exportlink($ID, 'code', array('codeblock' => $this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
644            $this->doc .= hsc($filename);
645            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
646        }
647
648        if($text{0} == "\n") {
649            $text = substr($text, 1);
650        }
651        if(substr($text, -1) == "\n") {
652            $text = substr($text, 0, -1);
653        }
654
655        if(is_null($language)) {
656            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
657        } else {
658            $class = 'code'; //we always need the code class to make the syntax highlighting apply
659            if($type != 'code') $class .= ' '.$type;
660
661            $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '').'</pre>'.DOKU_LF;
662        }
663
664        if($filename) {
665            $this->doc .= '</dd></dl>'.DOKU_LF;
666        }
667
668        $this->_codeblock++;
669    }
670
671    /**
672     * Format an acronym
673     *
674     * Uses $this->acronyms
675     *
676     * @param string $acronym
677     */
678    function acronym($acronym) {
679
680        if(array_key_exists($acronym, $this->acronyms)) {
681
682            $title = $this->_xmlEntities($this->acronyms[$acronym]);
683
684            $this->doc .= '<abbr title="'.$title
685                .'">'.$this->_xmlEntities($acronym).'</abbr>';
686
687        } else {
688            $this->doc .= $this->_xmlEntities($acronym);
689        }
690    }
691
692    /**
693     * Format a smiley
694     *
695     * Uses $this->smiley
696     *
697     * @param string $smiley
698     */
699    function smiley($smiley) {
700        if(array_key_exists($smiley, $this->smileys)) {
701            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
702                '" class="icon" alt="'.
703                $this->_xmlEntities($smiley).'" />';
704        } else {
705            $this->doc .= $this->_xmlEntities($smiley);
706        }
707    }
708
709    /**
710     * Format an entity
711     *
712     * Entities are basically small text replacements
713     *
714     * Uses $this->entities
715     *
716     * @param string $entity
717     */
718    function entity($entity) {
719        if(array_key_exists($entity, $this->entities)) {
720            $this->doc .= $this->entities[$entity];
721        } else {
722            $this->doc .= $this->_xmlEntities($entity);
723        }
724    }
725
726    /**
727     * Typographically format a multiply sign
728     *
729     * Example: ($x=640, $y=480) should result in "640×480"
730     *
731     * @param string|int $x first value
732     * @param string|int $y second value
733     */
734    function multiplyentity($x, $y) {
735        $this->doc .= "$x&times;$y";
736    }
737
738    /**
739     * Render an opening single quote char (language specific)
740     */
741    function singlequoteopening() {
742        global $lang;
743        $this->doc .= $lang['singlequoteopening'];
744    }
745
746    /**
747     * Render a closing single quote char (language specific)
748     */
749    function singlequoteclosing() {
750        global $lang;
751        $this->doc .= $lang['singlequoteclosing'];
752    }
753
754    /**
755     * Render an apostrophe char (language specific)
756     */
757    function apostrophe() {
758        global $lang;
759        $this->doc .= $lang['apostrophe'];
760    }
761
762    /**
763     * Render an opening double quote char (language specific)
764     */
765    function doublequoteopening() {
766        global $lang;
767        $this->doc .= $lang['doublequoteopening'];
768    }
769
770    /**
771     * Render an closinging double quote char (language specific)
772     */
773    function doublequoteclosing() {
774        global $lang;
775        $this->doc .= $lang['doublequoteclosing'];
776    }
777
778    /**
779     * Render a CamelCase link
780     *
781     * @param string $link       The link name
782     * @param bool   $returnonly whether to return html or write to doc attribute
783     * @return void|string writes to doc attribute or returns html depends on $returnonly
784     *
785     * @see http://en.wikipedia.org/wiki/CamelCase
786     */
787    function camelcaselink($link, $returnonly = false) {
788        if($returnonly) {
789          return $this->internallink($link, $link, null, true);
790        } else {
791          $this->internallink($link, $link);
792        }
793    }
794
795    /**
796     * Render a page local link
797     *
798     * @param string $hash       hash link identifier
799     * @param string $name       name for the link
800     * @param bool   $returnonly whether to return html or write to doc attribute
801     * @return void|string writes to doc attribute or returns html depends on $returnonly
802     */
803    function locallink($hash, $name = null, $returnonly = false) {
804        global $ID;
805        $name  = $this->_getLinkTitle($name, $hash, $isImage);
806        $hash  = $this->_headerToLink($hash);
807        $title = $ID.' ↵';
808
809        $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
810        $doc .= $name;
811        $doc .= '</a>';
812
813        if($returnonly) {
814          return $doc;
815        } else {
816          $this->doc .= $doc;
817        }
818    }
819
820    /**
821     * Render an internal Wiki Link
822     *
823     * $search,$returnonly & $linktype are not for the renderer but are used
824     * elsewhere - no need to implement them in other renderers
825     *
826     * @author Andreas Gohr <andi@splitbrain.org>
827     * @param string      $id         pageid
828     * @param string|null $name       link name
829     * @param string|null $search     adds search url param
830     * @param bool        $returnonly whether to return html or write to doc attribute
831     * @param string      $linktype   type to set use of headings
832     * @return void|string writes to doc attribute or returns html depends on $returnonly
833     */
834    function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
835        global $conf;
836        global $ID;
837        global $INFO;
838
839        $params = '';
840        $parts  = explode('?', $id, 2);
841        if(count($parts) === 2) {
842            $id     = $parts[0];
843            $params = $parts[1];
844        }
845
846        // For empty $id we need to know the current $ID
847        // We need this check because _simpleTitle needs
848        // correct $id and resolve_pageid() use cleanID($id)
849        // (some things could be lost)
850        if($id === '') {
851            $id = $ID;
852        }
853
854        // default name is based on $id as given
855        $default = $this->_simpleTitle($id);
856
857        // now first resolve and clean up the $id
858        resolve_pageid(getNS($ID), $id, $exists, $this->date_at, true);
859
860        $link = array();
861        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
862        if(!$isImage) {
863            if($exists) {
864                $class = 'wikilink1';
865            } else {
866                $class       = 'wikilink2';
867                $link['rel'] = 'nofollow';
868            }
869        } else {
870            $class = 'media';
871        }
872
873        //keep hash anchor
874        @list($id, $hash) = explode('#', $id, 2);
875        if(!empty($hash)) $hash = $this->_headerToLink($hash);
876
877        //prepare for formating
878        $link['target'] = $conf['target']['wiki'];
879        $link['style']  = '';
880        $link['pre']    = '';
881        $link['suf']    = '';
882        // highlight link to current page
883        if($id == $INFO['id']) {
884            $link['pre'] = '<span class="curid">';
885            $link['suf'] = '</span>';
886        }
887        $link['more']   = '';
888        $link['class']  = $class;
889        if($this->date_at) {
890            $params['at'] = $this->date_at;
891        }
892        $link['url']    = wl($id, $params);
893        $link['name']   = $name;
894        $link['title']  = $id;
895        //add search string
896        if($search) {
897            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
898            if(is_array($search)) {
899                $search = array_map('rawurlencode', $search);
900                $link['url'] .= 's[]='.join('&amp;s[]=', $search);
901            } else {
902                $link['url'] .= 's='.rawurlencode($search);
903            }
904        }
905
906        //keep hash
907        if($hash) $link['url'] .= '#'.$hash;
908
909        //output formatted
910        if($returnonly) {
911            return $this->_formatLink($link);
912        } else {
913            $this->doc .= $this->_formatLink($link);
914        }
915    }
916
917    /**
918     * Render an external link
919     *
920     * @param string       $url        full URL with scheme
921     * @param string|array $name       name for the link, array for media file
922     * @param bool         $returnonly whether to return html or write to doc attribute
923     * @return void|string writes to doc attribute or returns html depends on $returnonly
924     */
925    function externallink($url, $name = null, $returnonly = false) {
926        global $conf;
927
928        $name = $this->_getLinkTitle($name, $url, $isImage);
929
930        // url might be an attack vector, only allow registered protocols
931        if(is_null($this->schemes)) $this->schemes = getSchemes();
932        list($scheme) = explode('://', $url);
933        $scheme = strtolower($scheme);
934        if(!in_array($scheme, $this->schemes)) $url = '';
935
936        // is there still an URL?
937        if(!$url) {
938            if($returnonly) {
939                return $name;
940            } else {
941                $this->doc .= $name;
942            }
943            return;
944        }
945
946        // set class
947        if(!$isImage) {
948            $class = 'urlextern';
949        } else {
950            $class = 'media';
951        }
952
953        //prepare for formating
954        $link = array();
955        $link['target'] = $conf['target']['extern'];
956        $link['style']  = '';
957        $link['pre']    = '';
958        $link['suf']    = '';
959        $link['more']   = '';
960        $link['class']  = $class;
961        $link['url']    = $url;
962        $link['rel']    = '';
963
964        $link['name']  = $name;
965        $link['title'] = $this->_xmlEntities($url);
966        if($conf['relnofollow']) $link['rel'] .= ' nofollow';
967        if($conf['target']['extern']) $link['rel'] .= ' noopener';
968
969        //output formatted
970        if($returnonly) {
971            return $this->_formatLink($link);
972        } else {
973            $this->doc .= $this->_formatLink($link);
974        }
975    }
976
977    /**
978     * Render an interwiki link
979     *
980     * You may want to use $this->_resolveInterWiki() here
981     *
982     * @param string       $match      original link - probably not much use
983     * @param string|array $name       name for the link, array for media file
984     * @param string       $wikiName   indentifier (shortcut) for the remote wiki
985     * @param string       $wikiUri    the fragment parsed from the original link
986     * @param bool         $returnonly whether to return html or write to doc attribute
987     * @return void|string writes to doc attribute or returns html depends on $returnonly
988     */
989    function interwikilink($match, $name = null, $wikiName, $wikiUri, $returnonly = false) {
990        global $conf;
991
992        $link           = array();
993        $link['target'] = $conf['target']['interwiki'];
994        $link['pre']    = '';
995        $link['suf']    = '';
996        $link['more']   = '';
997        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
998        $link['rel']    = '';
999
1000        //get interwiki URL
1001        $exists = null;
1002        $url    = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
1003
1004        if(!$isImage) {
1005            $class         = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
1006            $link['class'] = "interwiki iw_$class";
1007        } else {
1008            $link['class'] = 'media';
1009        }
1010
1011        //do we stay at the same server? Use local target
1012        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
1013            $link['target'] = $conf['target']['wiki'];
1014        }
1015        if($exists !== null && !$isImage) {
1016            if($exists) {
1017                $link['class'] .= ' wikilink1';
1018            } else {
1019                $link['class'] .= ' wikilink2';
1020                $link['rel'] .= ' nofollow';
1021            }
1022        }
1023        if($conf['target']['interwiki']) $link['rel'] .= ' noopener';
1024
1025        $link['url']   = $url;
1026        $link['title'] = htmlspecialchars($link['url']);
1027
1028        //output formatted
1029        if($returnonly) {
1030            return $this->_formatLink($link);
1031        } else {
1032            $this->doc .= $this->_formatLink($link);
1033        }
1034    }
1035
1036    /**
1037     * Link to windows share
1038     *
1039     * @param string       $url        the link
1040     * @param string|array $name       name for the link, array for media file
1041     * @param bool         $returnonly whether to return html or write to doc attribute
1042     * @return void|string writes to doc attribute or returns html depends on $returnonly
1043     */
1044    function windowssharelink($url, $name = null, $returnonly = false) {
1045        global $conf;
1046
1047        //simple setup
1048        $link = array();
1049        $link['target'] = $conf['target']['windows'];
1050        $link['pre']    = '';
1051        $link['suf']    = '';
1052        $link['style']  = '';
1053
1054        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
1055        if(!$isImage) {
1056            $link['class'] = 'windows';
1057        } else {
1058            $link['class'] = 'media';
1059        }
1060
1061        $link['title'] = $this->_xmlEntities($url);
1062        $url           = str_replace('\\', '/', $url);
1063        $url           = 'file:///'.$url;
1064        $link['url']   = $url;
1065
1066        //output formatted
1067        if($returnonly) {
1068            return $this->_formatLink($link);
1069        } else {
1070            $this->doc .= $this->_formatLink($link);
1071        }
1072    }
1073
1074    /**
1075     * Render a linked E-Mail Address
1076     *
1077     * Honors $conf['mailguard'] setting
1078     *
1079     * @param string       $address    Email-Address
1080     * @param string|array $name       name for the link, array for media file
1081     * @param bool         $returnonly whether to return html or write to doc attribute
1082     * @return void|string writes to doc attribute or returns html depends on $returnonly
1083     */
1084    function emaillink($address, $name = null, $returnonly = false) {
1085        global $conf;
1086        //simple setup
1087        $link           = array();
1088        $link['target'] = '';
1089        $link['pre']    = '';
1090        $link['suf']    = '';
1091        $link['style']  = '';
1092        $link['more']   = '';
1093
1094        $name = $this->_getLinkTitle($name, '', $isImage);
1095        if(!$isImage) {
1096            $link['class'] = 'mail';
1097        } else {
1098            $link['class'] = 'media';
1099        }
1100
1101        $address = $this->_xmlEntities($address);
1102        $address = obfuscate($address);
1103        $title   = $address;
1104
1105        if(empty($name)) {
1106            $name = $address;
1107        }
1108
1109        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
1110
1111        $link['url']   = 'mailto:'.$address;
1112        $link['name']  = $name;
1113        $link['title'] = $title;
1114
1115        //output formatted
1116        if($returnonly) {
1117            return $this->_formatLink($link);
1118        } else {
1119            $this->doc .= $this->_formatLink($link);
1120        }
1121    }
1122
1123    /**
1124     * Render an internal media file
1125     *
1126     * @param string $src       media ID
1127     * @param string $title     descriptive text
1128     * @param string $align     left|center|right
1129     * @param int    $width     width of media in pixel
1130     * @param int    $height    height of media in pixel
1131     * @param string $cache     cache|recache|nocache
1132     * @param string $linking   linkonly|detail|nolink
1133     * @param bool   $return    return HTML instead of adding to $doc
1134     * @return void|string writes to doc attribute or returns html depends on $return
1135     */
1136    function internalmedia($src, $title = null, $align = null, $width = null,
1137                           $height = null, $cache = null, $linking = null, $return = false) {
1138        global $ID;
1139        if (strpos($src, '#') !== false) {
1140            list($src, $hash) = explode('#', $src, 2);
1141        }
1142        resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true);
1143
1144        $noLink = false;
1145        $render = ($linking == 'linkonly') ? false : true;
1146        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1147
1148        list($ext, $mime) = mimetype($src, false);
1149        if(substr($mime, 0, 5) == 'image' && $render) {
1150            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src)), ($linking == 'direct'));
1151        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1152            // don't link movies
1153            $noLink = true;
1154        } else {
1155            // add file icons
1156            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1157            $link['class'] .= ' mediafile mf_'.$class;
1158            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache , 'rev'=>$this->_getLastMediaRevisionAt($src)), true);
1159            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
1160        }
1161
1162        if (!empty($hash)) $link['url'] .= '#'.$hash;
1163
1164        //markup non existing files
1165        if(!$exists) {
1166            $link['class'] .= ' wikilink2';
1167        }
1168
1169        //output formatted
1170        if($return) {
1171            if($linking == 'nolink' || $noLink) return $link['name'];
1172            else return $this->_formatLink($link);
1173        } else {
1174            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1175            else $this->doc .= $this->_formatLink($link);
1176        }
1177    }
1178
1179    /**
1180     * Render an external media file
1181     *
1182     * @param string $src     full media URL
1183     * @param string $title   descriptive text
1184     * @param string $align   left|center|right
1185     * @param int    $width   width of media in pixel
1186     * @param int    $height  height of media in pixel
1187     * @param string $cache   cache|recache|nocache
1188     * @param string $linking linkonly|detail|nolink
1189     * @param bool   $return  return HTML instead of adding to $doc
1190     * @return void|string writes to doc attribute or returns html depends on $return
1191     */
1192    function externalmedia($src, $title = null, $align = null, $width = null,
1193                           $height = null, $cache = null, $linking = null, $return = false) {
1194        if(link_isinterwiki($src)){
1195            list($shortcut, $reference) = explode('>', $src, 2);
1196            $exists = null;
1197            $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
1198        }
1199        list($src, $hash) = explode('#', $src, 2);
1200        $noLink = false;
1201        $render = ($linking == 'linkonly') ? false : true;
1202        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1203
1204        $link['url'] = ml($src, array('cache' => $cache));
1205
1206        list($ext, $mime) = mimetype($src, false);
1207        if(substr($mime, 0, 5) == 'image' && $render) {
1208            // link only jpeg images
1209            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
1210        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1211            // don't link movies
1212            $noLink = true;
1213        } else {
1214            // add file icons
1215            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1216            $link['class'] .= ' mediafile mf_'.$class;
1217        }
1218
1219        if($hash) $link['url'] .= '#'.$hash;
1220
1221        //output formatted
1222        if($return) {
1223            if($linking == 'nolink' || $noLink) return $link['name'];
1224            else return $this->_formatLink($link);
1225        } else {
1226            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1227            else $this->doc .= $this->_formatLink($link);
1228        }
1229    }
1230
1231    /**
1232     * Renders an RSS feed
1233     *
1234     * @param string $url    URL of the feed
1235     * @param array  $params Finetuning of the output
1236     *
1237     * @author Andreas Gohr <andi@splitbrain.org>
1238     */
1239    function rss($url, $params) {
1240        global $lang;
1241        global $conf;
1242
1243        require_once(DOKU_INC.'inc/FeedParser.php');
1244        $feed = new FeedParser();
1245        $feed->set_feed_url($url);
1246
1247        //disable warning while fetching
1248        if(!defined('DOKU_E_LEVEL')) {
1249            $elvl = error_reporting(E_ERROR);
1250        }
1251        $rc = $feed->init();
1252        if(isset($elvl)) {
1253            error_reporting($elvl);
1254        }
1255
1256        if($params['nosort']) $feed->enable_order_by_date(false);
1257
1258        //decide on start and end
1259        if($params['reverse']) {
1260            $mod   = -1;
1261            $start = $feed->get_item_quantity() - 1;
1262            $end   = $start - ($params['max']);
1263            $end   = ($end < -1) ? -1 : $end;
1264        } else {
1265            $mod   = 1;
1266            $start = 0;
1267            $end   = $feed->get_item_quantity();
1268            $end   = ($end > $params['max']) ? $params['max'] : $end;
1269        }
1270
1271        $this->doc .= '<ul class="rss">';
1272        if($rc) {
1273            for($x = $start; $x != $end; $x += $mod) {
1274                $item = $feed->get_item($x);
1275                $this->doc .= '<li><div class="li">';
1276                // support feeds without links
1277                $lnkurl = $item->get_permalink();
1278                if($lnkurl) {
1279                    // title is escaped by SimplePie, we unescape here because it
1280                    // is escaped again in externallink() FS#1705
1281                    $this->externallink(
1282                        $item->get_permalink(),
1283                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
1284                    );
1285                } else {
1286                    $this->doc .= ' '.$item->get_title();
1287                }
1288                if($params['author']) {
1289                    $author = $item->get_author(0);
1290                    if($author) {
1291                        $name = $author->get_name();
1292                        if(!$name) $name = $author->get_email();
1293                        if($name) $this->doc .= ' '.$lang['by'].' '.$name;
1294                    }
1295                }
1296                if($params['date']) {
1297                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1298                }
1299                if($params['details']) {
1300                    $this->doc .= '<div class="detail">';
1301                    if($conf['htmlok']) {
1302                        $this->doc .= $item->get_description();
1303                    } else {
1304                        $this->doc .= strip_tags($item->get_description());
1305                    }
1306                    $this->doc .= '</div>';
1307                }
1308
1309                $this->doc .= '</div></li>';
1310            }
1311        } else {
1312            $this->doc .= '<li><div class="li">';
1313            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1314            $this->externallink($url);
1315            if($conf['allowdebug']) {
1316                $this->doc .= '<!--'.hsc($feed->error).'-->';
1317            }
1318            $this->doc .= '</div></li>';
1319        }
1320        $this->doc .= '</ul>';
1321    }
1322
1323    /**
1324     * Start a table
1325     *
1326     * @param int $maxcols maximum number of columns
1327     * @param int $numrows NOT IMPLEMENTED
1328     * @param int $pos byte position in the original source
1329     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1330     */
1331    function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
1332        // initialize the row counter used for classes
1333        $this->_counter['row_counter'] = 0;
1334        $class                         = 'table';
1335        if($classes !== null) {
1336            if(is_array($classes)) $classes = join(' ', $classes);
1337            $class .= ' ' . $classes;
1338        }
1339        if($pos !== null) {
1340            $class .= ' '.$this->startSectionEdit($pos, 'table');
1341        }
1342        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1343            DOKU_LF;
1344    }
1345
1346    /**
1347     * Close a table
1348     *
1349     * @param int $pos byte position in the original source
1350     */
1351    function table_close($pos = null) {
1352        $this->doc .= '</table></div>'.DOKU_LF;
1353        if($pos !== null) {
1354            $this->finishSectionEdit($pos);
1355        }
1356    }
1357
1358    /**
1359     * Open a table header
1360     */
1361    function tablethead_open() {
1362        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1363    }
1364
1365    /**
1366     * Close a table header
1367     */
1368    function tablethead_close() {
1369        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1370    }
1371
1372    /**
1373     * Open a table body
1374     */
1375    function tabletbody_open() {
1376        $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
1377    }
1378
1379    /**
1380     * Close a table body
1381     */
1382    function tabletbody_close() {
1383        $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
1384    }
1385
1386    /**
1387     * Open a table footer
1388     */
1389    function tabletfoot_open() {
1390        $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
1391    }
1392
1393    /**
1394     * Close a table footer
1395     */
1396    function tabletfoot_close() {
1397        $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
1398    }
1399
1400    /**
1401     * Open a table row
1402     *
1403     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1404     */
1405    function tablerow_open($classes = null) {
1406        // initialize the cell counter used for classes
1407        $this->_counter['cell_counter'] = 0;
1408        $class                          = 'row'.$this->_counter['row_counter']++;
1409        if($classes !== null) {
1410            if(is_array($classes)) $classes = join(' ', $classes);
1411            $class .= ' ' . $classes;
1412        }
1413        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1414    }
1415
1416    /**
1417     * Close a table row
1418     */
1419    function tablerow_close() {
1420        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1421    }
1422
1423    /**
1424     * Open a table header cell
1425     *
1426     * @param int    $colspan
1427     * @param string $align left|center|right
1428     * @param int    $rowspan
1429     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1430     */
1431    function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1432        $class = 'class="col'.$this->_counter['cell_counter']++;
1433        if(!is_null($align)) {
1434            $class .= ' '.$align.'align';
1435        }
1436        if($classes !== null) {
1437            if(is_array($classes)) $classes = join(' ', $classes);
1438            $class .= ' ' . $classes;
1439        }
1440        $class .= '"';
1441        $this->doc .= '<th '.$class;
1442        if($colspan > 1) {
1443            $this->_counter['cell_counter'] += $colspan - 1;
1444            $this->doc .= ' colspan="'.$colspan.'"';
1445        }
1446        if($rowspan > 1) {
1447            $this->doc .= ' rowspan="'.$rowspan.'"';
1448        }
1449        $this->doc .= '>';
1450    }
1451
1452    /**
1453     * Close a table header cell
1454     */
1455    function tableheader_close() {
1456        $this->doc .= '</th>';
1457    }
1458
1459    /**
1460     * Open a table cell
1461     *
1462     * @param int       $colspan
1463     * @param string    $align left|center|right
1464     * @param int       $rowspan
1465     * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
1466     */
1467    function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1468        $class = 'class="col'.$this->_counter['cell_counter']++;
1469        if(!is_null($align)) {
1470            $class .= ' '.$align.'align';
1471        }
1472        if($classes !== null) {
1473            if(is_array($classes)) $classes = join(' ', $classes);
1474            $class .= ' ' . $classes;
1475        }
1476        $class .= '"';
1477        $this->doc .= '<td '.$class;
1478        if($colspan > 1) {
1479            $this->_counter['cell_counter'] += $colspan - 1;
1480            $this->doc .= ' colspan="'.$colspan.'"';
1481        }
1482        if($rowspan > 1) {
1483            $this->doc .= ' rowspan="'.$rowspan.'"';
1484        }
1485        $this->doc .= '>';
1486    }
1487
1488    /**
1489     * Close a table cell
1490     */
1491    function tablecell_close() {
1492        $this->doc .= '</td>';
1493    }
1494
1495    /**
1496     * Returns the current header level.
1497     * (required e.g. by the filelist plugin)
1498     *
1499     * @return int The current header level
1500     */
1501    function getLastlevel() {
1502        return $this->lastlevel;
1503    }
1504
1505    #region Utility functions
1506
1507    /**
1508     * Build a link
1509     *
1510     * Assembles all parts defined in $link returns HTML for the link
1511     *
1512     * @param array $link attributes of a link
1513     * @return string
1514     *
1515     * @author Andreas Gohr <andi@splitbrain.org>
1516     */
1517    function _formatLink($link) {
1518        //make sure the url is XHTML compliant (skip mailto)
1519        if(substr($link['url'], 0, 7) != 'mailto:') {
1520            $link['url'] = str_replace('&', '&amp;', $link['url']);
1521            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1522        }
1523        //remove double encodings in titles
1524        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1525
1526        // be sure there are no bad chars in url or title
1527        // (we can't do this for name because it can contain an img tag)
1528        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1529        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1530
1531        $ret = '';
1532        $ret .= $link['pre'];
1533        $ret .= '<a href="'.$link['url'].'"';
1534        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1535        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1536        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1537        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1538        if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
1539        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1540        $ret .= '>';
1541        $ret .= $link['name'];
1542        $ret .= '</a>';
1543        $ret .= $link['suf'];
1544        return $ret;
1545    }
1546
1547    /**
1548     * Renders internal and external media
1549     *
1550     * @author Andreas Gohr <andi@splitbrain.org>
1551     * @param string $src       media ID
1552     * @param string $title     descriptive text
1553     * @param string $align     left|center|right
1554     * @param int    $width     width of media in pixel
1555     * @param int    $height    height of media in pixel
1556     * @param string $cache     cache|recache|nocache
1557     * @param bool   $render    should the media be embedded inline or just linked
1558     * @return string
1559     */
1560    function _media($src, $title = null, $align = null, $width = null,
1561                    $height = null, $cache = null, $render = true) {
1562
1563        $ret = '';
1564
1565        list($ext, $mime) = mimetype($src);
1566        if(substr($mime, 0, 5) == 'image') {
1567            // first get the $title
1568            if(!is_null($title)) {
1569                $title = $this->_xmlEntities($title);
1570            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1571                //try to use the caption from IPTC/EXIF
1572                require_once(DOKU_INC.'inc/JpegMeta.php');
1573                $jpeg = new JpegMeta(mediaFN($src));
1574                if($jpeg !== false) $cap = $jpeg->getTitle();
1575                if(!empty($cap)) {
1576                    $title = $this->_xmlEntities($cap);
1577                }
1578            }
1579            if(!$render) {
1580                // if the picture is not supposed to be rendered
1581                // return the title of the picture
1582                if(!$title) {
1583                    // just show the sourcename
1584                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1585                }
1586                return $title;
1587            }
1588            //add image tag
1589            $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"';
1590            $ret .= ' class="media'.$align.'"';
1591
1592            if($title) {
1593                $ret .= ' title="'.$title.'"';
1594                $ret .= ' alt="'.$title.'"';
1595            } else {
1596                $ret .= ' alt=""';
1597            }
1598
1599            if(!is_null($width))
1600                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1601
1602            if(!is_null($height))
1603                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1604
1605            $ret .= ' />';
1606
1607        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1608            // first get the $title
1609            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1610            if(!$render) {
1611                // if the file is not supposed to be rendered
1612                // return the title of the file (just the sourcename if there is no title)
1613                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1614            }
1615
1616            $att          = array();
1617            $att['class'] = "media$align";
1618            if($title) {
1619                $att['title'] = $title;
1620            }
1621
1622            if(media_supportedav($mime, 'video')) {
1623                //add video
1624                $ret .= $this->_video($src, $width, $height, $att);
1625            }
1626            if(media_supportedav($mime, 'audio')) {
1627                //add audio
1628                $ret .= $this->_audio($src, $att);
1629            }
1630
1631        } elseif($mime == 'application/x-shockwave-flash') {
1632            if(!$render) {
1633                // if the flash is not supposed to be rendered
1634                // return the title of the flash
1635                if(!$title) {
1636                    // just show the sourcename
1637                    $title = utf8_basename(noNS($src));
1638                }
1639                return $this->_xmlEntities($title);
1640            }
1641
1642            $att          = array();
1643            $att['class'] = "media$align";
1644            if($align == 'right') $att['align'] = 'right';
1645            if($align == 'left') $att['align'] = 'left';
1646            $ret .= html_flashobject(
1647                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1648                array('quality' => 'high'),
1649                null,
1650                $att,
1651                $this->_xmlEntities($title)
1652            );
1653        } elseif($title) {
1654            // well at least we have a title to display
1655            $ret .= $this->_xmlEntities($title);
1656        } else {
1657            // just show the sourcename
1658            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1659        }
1660
1661        return $ret;
1662    }
1663
1664    /**
1665     * Escape string for output
1666     *
1667     * @param $string
1668     * @return string
1669     */
1670    function _xmlEntities($string) {
1671        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1672    }
1673
1674    /**
1675     * Creates a linkid from a headline
1676     *
1677     * @author Andreas Gohr <andi@splitbrain.org>
1678     * @param string  $title   The headline title
1679     * @param boolean $create  Create a new unique ID?
1680     * @return string
1681     */
1682    function _headerToLink($title, $create = false) {
1683        if($create) {
1684            return sectionID($title, $this->headers);
1685        } else {
1686            $check = false;
1687            return sectionID($title, $check);
1688        }
1689    }
1690
1691    /**
1692     * Construct a title and handle images in titles
1693     *
1694     * @author Harry Fuecks <hfuecks@gmail.com>
1695     * @param string|array $title    either string title or media array
1696     * @param string       $default  default title if nothing else is found
1697     * @param bool         $isImage  will be set to true if it's a media file
1698     * @param null|string  $id       linked page id (used to extract title from first heading)
1699     * @param string       $linktype content|navigation
1700     * @return string      HTML of the title, might be full image tag or just escaped text
1701     */
1702    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1703        $isImage = false;
1704        if(is_array($title)) {
1705            $isImage = true;
1706            return $this->_imageTitle($title);
1707        } elseif(is_null($title) || trim($title) == '') {
1708            if(useHeading($linktype) && $id) {
1709                $heading = p_get_first_heading($id);
1710                if(!blank($heading)) {
1711                    return $this->_xmlEntities($heading);
1712                }
1713            }
1714            return $this->_xmlEntities($default);
1715        } else {
1716            return $this->_xmlEntities($title);
1717        }
1718    }
1719
1720    /**
1721     * Returns HTML code for images used in link titles
1722     *
1723     * @author Andreas Gohr <andi@splitbrain.org>
1724     * @param array $img
1725     * @return string HTML img tag or similar
1726     */
1727    function _imageTitle($img) {
1728        global $ID;
1729
1730        // some fixes on $img['src']
1731        // see internalmedia() and externalmedia()
1732        list($img['src']) = explode('#', $img['src'], 2);
1733        if($img['type'] == 'internalmedia') {
1734            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
1735        }
1736
1737        return $this->_media(
1738            $img['src'],
1739            $img['title'],
1740            $img['align'],
1741            $img['width'],
1742            $img['height'],
1743            $img['cache']
1744        );
1745    }
1746
1747    /**
1748     * helperfunction to return a basic link to a media
1749     *
1750     * used in internalmedia() and externalmedia()
1751     *
1752     * @author   Pierre Spring <pierre.spring@liip.ch>
1753     * @param string $src       media ID
1754     * @param string $title     descriptive text
1755     * @param string $align     left|center|right
1756     * @param int    $width     width of media in pixel
1757     * @param int    $height    height of media in pixel
1758     * @param string $cache     cache|recache|nocache
1759     * @param bool   $render    should the media be embedded inline or just linked
1760     * @return array associative array with link config
1761     */
1762    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1763        global $conf;
1764
1765        $link           = array();
1766        $link['class']  = 'media';
1767        $link['style']  = '';
1768        $link['pre']    = '';
1769        $link['suf']    = '';
1770        $link['more']   = '';
1771        $link['target'] = $conf['target']['media'];
1772        if($conf['target']['media']) $link['rel'] = 'noopener';
1773        $link['title']  = $this->_xmlEntities($src);
1774        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1775
1776        return $link;
1777    }
1778
1779    /**
1780     * Embed video(s) in HTML
1781     *
1782     * @author Anika Henke <anika@selfthinker.org>
1783     *
1784     * @param string $src         - ID of video to embed
1785     * @param int    $width       - width of the video in pixels
1786     * @param int    $height      - height of the video in pixels
1787     * @param array  $atts        - additional attributes for the <video> tag
1788     * @return string
1789     */
1790    function _video($src, $width, $height, $atts = null) {
1791        // prepare width and height
1792        if(is_null($atts)) $atts = array();
1793        $atts['width']  = (int) $width;
1794        $atts['height'] = (int) $height;
1795        if(!$atts['width']) $atts['width'] = 320;
1796        if(!$atts['height']) $atts['height'] = 240;
1797
1798        $posterUrl = '';
1799        $files = array();
1800        $isExternal = media_isexternal($src);
1801
1802        if ($isExternal) {
1803            // take direct source for external files
1804            list(/*ext*/, $srcMime) = mimetype($src);
1805            $files[$srcMime] = $src;
1806        } else {
1807            // prepare alternative formats
1808            $extensions   = array('webm', 'ogv', 'mp4');
1809            $files        = media_alternativefiles($src, $extensions);
1810            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1811            if(!empty($poster)) {
1812                $posterUrl = ml(reset($poster), '', true, '&');
1813            }
1814        }
1815
1816        $out = '';
1817        // open video tag
1818        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1819        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1820        $out .= '>'.NL;
1821        $fallback = '';
1822
1823        // output source for each alternative video format
1824        foreach($files as $mime => $file) {
1825            if ($isExternal) {
1826                $url = $file;
1827                $linkType = 'externalmedia';
1828            } else {
1829                $url = ml($file, '', true, '&');
1830                $linkType = 'internalmedia';
1831            }
1832            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1833
1834            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1835            // alternative content (just a link to the file)
1836            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1837        }
1838
1839        // finish
1840        $out .= $fallback;
1841        $out .= '</video>'.NL;
1842        return $out;
1843    }
1844
1845    /**
1846     * Embed audio in HTML
1847     *
1848     * @author Anika Henke <anika@selfthinker.org>
1849     *
1850     * @param string $src       - ID of audio to embed
1851     * @param array  $atts      - additional attributes for the <audio> tag
1852     * @return string
1853     */
1854    function _audio($src, $atts = array()) {
1855        $files = array();
1856        $isExternal = media_isexternal($src);
1857
1858        if ($isExternal) {
1859            // take direct source for external files
1860            list(/*ext*/, $srcMime) = mimetype($src);
1861            $files[$srcMime] = $src;
1862        } else {
1863            // prepare alternative formats
1864            $extensions   = array('ogg', 'mp3', 'wav');
1865            $files        = media_alternativefiles($src, $extensions);
1866        }
1867
1868        $out = '';
1869        // open audio tag
1870        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1871        $fallback = '';
1872
1873        // output source for each alternative audio format
1874        foreach($files as $mime => $file) {
1875            if ($isExternal) {
1876                $url = $file;
1877                $linkType = 'externalmedia';
1878            } else {
1879                $url = ml($file, '', true, '&');
1880                $linkType = 'internalmedia';
1881            }
1882            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1883
1884            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1885            // alternative content (just a link to the file)
1886            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1887        }
1888
1889        // finish
1890        $out .= $fallback;
1891        $out .= '</audio>'.NL;
1892        return $out;
1893    }
1894
1895    /**
1896     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1897     * which returns an existing media revision less or equal to rev or date_at
1898     *
1899     * @author lisps
1900     * @param string $media_id
1901     * @access protected
1902     * @return string revision ('' for current)
1903     */
1904    function _getLastMediaRevisionAt($media_id){
1905        if(!$this->date_at || media_isexternal($media_id)) return '';
1906        $pagelog = new MediaChangeLog($media_id);
1907        return $pagelog->getLastRevisionAt($this->date_at);
1908    }
1909
1910    #endregion
1911}
1912
1913//Setup VIM: ex: et ts=4 :
1914