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