xref: /dokuwiki/inc/parser/xhtml.php (revision 6c8c1f4632a82459236200e9264c40ecebd4b162)
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           = 'file:///'.$url;
1003        $link['url']   = $url;
1004
1005        //output formatted
1006        $this->doc .= $this->_formatLink($link);
1007    }
1008
1009    /**
1010     * Render a linked E-Mail Address
1011     *
1012     * Honors $conf['mailguard'] setting
1013     *
1014     * @param string       $address Email-Address
1015     * @param string|array $name    name for the link, array for media file
1016     */
1017    function emaillink($address, $name = null) {
1018        global $conf;
1019        //simple setup
1020        $link           = array();
1021        $link['target'] = '';
1022        $link['pre']    = '';
1023        $link['suf']    = '';
1024        $link['style']  = '';
1025        $link['more']   = '';
1026
1027        $name = $this->_getLinkTitle($name, '', $isImage);
1028        if(!$isImage) {
1029            $link['class'] = 'mail';
1030        } else {
1031            $link['class'] = 'media';
1032        }
1033
1034        $address = $this->_xmlEntities($address);
1035        $address = obfuscate($address);
1036        $title   = $address;
1037
1038        if(empty($name)) {
1039            $name = $address;
1040        }
1041
1042        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
1043
1044        $link['url']   = 'mailto:'.$address;
1045        $link['name']  = $name;
1046        $link['title'] = $title;
1047
1048        //output formatted
1049        $this->doc .= $this->_formatLink($link);
1050    }
1051
1052    /**
1053     * Render an internal media file
1054     *
1055     * @param string $src       media ID
1056     * @param string $title     descriptive text
1057     * @param string $align     left|center|right
1058     * @param int    $width     width of media in pixel
1059     * @param int    $height    height of media in pixel
1060     * @param string $cache     cache|recache|nocache
1061     * @param string $linking   linkonly|detail|nolink
1062     * @param bool   $return    return HTML instead of adding to $doc
1063     * @return void|string
1064     */
1065    function internalmedia($src, $title = null, $align = null, $width = null,
1066                           $height = null, $cache = null, $linking = null, $return = false) {
1067        global $ID;
1068        list($src, $hash) = explode('#', $src, 2);
1069        resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true);
1070
1071        $noLink = false;
1072        $render = ($linking == 'linkonly') ? false : true;
1073        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1074
1075        list($ext, $mime) = mimetype($src, false);
1076        if(substr($mime, 0, 5) == 'image' && $render) {
1077            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src)), ($linking == 'direct'));
1078        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1079            // don't link movies
1080            $noLink = true;
1081        } else {
1082            // add file icons
1083            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1084            $link['class'] .= ' mediafile mf_'.$class;
1085            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache , 'rev'=>$this->_getLastMediaRevisionAt($src)), true);
1086            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
1087        }
1088
1089        if($hash) $link['url'] .= '#'.$hash;
1090
1091        //markup non existing files
1092        if(!$exists) {
1093            $link['class'] .= ' wikilink2';
1094        }
1095
1096        //output formatted
1097        if($return) {
1098            if($linking == 'nolink' || $noLink) return $link['name'];
1099            else return $this->_formatLink($link);
1100        } else {
1101            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1102            else $this->doc .= $this->_formatLink($link);
1103        }
1104    }
1105
1106    /**
1107     * Render an external media file
1108     *
1109     * @param string $src     full media URL
1110     * @param string $title   descriptive text
1111     * @param string $align   left|center|right
1112     * @param int    $width   width of media in pixel
1113     * @param int    $height  height of media in pixel
1114     * @param string $cache   cache|recache|nocache
1115     * @param string $linking linkonly|detail|nolink
1116     * @param bool   $return  return HTML instead of adding to $doc
1117     */
1118    function externalmedia($src, $title = null, $align = null, $width = null,
1119                           $height = null, $cache = null, $linking = null, $return = false) {
1120        list($src, $hash) = explode('#', $src, 2);
1121        $noLink = false;
1122        $render = ($linking == 'linkonly') ? false : true;
1123        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1124
1125        $link['url'] = ml($src, array('cache' => $cache));
1126
1127        list($ext, $mime) = mimetype($src, false);
1128        if(substr($mime, 0, 5) == 'image' && $render) {
1129            // link only jpeg images
1130            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
1131        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1132            // don't link movies
1133            $noLink = true;
1134        } else {
1135            // add file icons
1136            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1137            $link['class'] .= ' mediafile mf_'.$class;
1138        }
1139
1140        if($hash) $link['url'] .= '#'.$hash;
1141
1142        //output formatted
1143        if($return) {
1144            if($linking == 'nolink' || $noLink) return $link['name'];
1145            else return $this->_formatLink($link);
1146        } else {
1147            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1148            else $this->doc .= $this->_formatLink($link);
1149        }
1150    }
1151
1152    /**
1153     * Renders an RSS feed
1154     *
1155     * @author Andreas Gohr <andi@splitbrain.org>
1156     */
1157    function rss($url, $params) {
1158        global $lang;
1159        global $conf;
1160
1161        require_once(DOKU_INC.'inc/FeedParser.php');
1162        $feed = new FeedParser();
1163        $feed->set_feed_url($url);
1164
1165        //disable warning while fetching
1166        if(!defined('DOKU_E_LEVEL')) {
1167            $elvl = error_reporting(E_ERROR);
1168        }
1169        $rc = $feed->init();
1170        if(isset($elvl)) {
1171            error_reporting($elvl);
1172        }
1173
1174        //decide on start and end
1175        if($params['reverse']) {
1176            $mod   = -1;
1177            $start = $feed->get_item_quantity() - 1;
1178            $end   = $start - ($params['max']);
1179            $end   = ($end < -1) ? -1 : $end;
1180        } else {
1181            $mod   = 1;
1182            $start = 0;
1183            $end   = $feed->get_item_quantity();
1184            $end   = ($end > $params['max']) ? $params['max'] : $end;
1185        }
1186
1187        $this->doc .= '<ul class="rss">';
1188        if($rc) {
1189            for($x = $start; $x != $end; $x += $mod) {
1190                $item = $feed->get_item($x);
1191                $this->doc .= '<li><div class="li">';
1192                // support feeds without links
1193                $lnkurl = $item->get_permalink();
1194                if($lnkurl) {
1195                    // title is escaped by SimplePie, we unescape here because it
1196                    // is escaped again in externallink() FS#1705
1197                    $this->externallink(
1198                        $item->get_permalink(),
1199                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
1200                    );
1201                } else {
1202                    $this->doc .= ' '.$item->get_title();
1203                }
1204                if($params['author']) {
1205                    $author = $item->get_author(0);
1206                    if($author) {
1207                        $name = $author->get_name();
1208                        if(!$name) $name = $author->get_email();
1209                        if($name) $this->doc .= ' '.$lang['by'].' '.$name;
1210                    }
1211                }
1212                if($params['date']) {
1213                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1214                }
1215                if($params['details']) {
1216                    $this->doc .= '<div class="detail">';
1217                    if($conf['htmlok']) {
1218                        $this->doc .= $item->get_description();
1219                    } else {
1220                        $this->doc .= strip_tags($item->get_description());
1221                    }
1222                    $this->doc .= '</div>';
1223                }
1224
1225                $this->doc .= '</div></li>';
1226            }
1227        } else {
1228            $this->doc .= '<li><div class="li">';
1229            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1230            $this->externallink($url);
1231            if($conf['allowdebug']) {
1232                $this->doc .= '<!--'.hsc($feed->error).'-->';
1233            }
1234            $this->doc .= '</div></li>';
1235        }
1236        $this->doc .= '</ul>';
1237    }
1238
1239    /**
1240     * Start a table
1241     *
1242     * @param int $maxcols maximum number of columns
1243     * @param int $numrows NOT IMPLEMENTED
1244     * @param int $pos     byte position in the original source
1245     */
1246    function table_open($maxcols = null, $numrows = null, $pos = null) {
1247        // initialize the row counter used for classes
1248        $this->_counter['row_counter'] = 0;
1249        $class                         = 'table';
1250        if($pos !== null) {
1251            $class .= ' '.$this->startSectionEdit($pos, 'table');
1252        }
1253        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1254            DOKU_LF;
1255    }
1256
1257    /**
1258     * Close a table
1259     *
1260     * @param int $pos byte position in the original source
1261     */
1262    function table_close($pos = null) {
1263        $this->doc .= '</table></div>'.DOKU_LF;
1264        if($pos !== null) {
1265            $this->finishSectionEdit($pos);
1266        }
1267    }
1268
1269    /**
1270     * Open a table header
1271     */
1272    function tablethead_open() {
1273        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1274    }
1275
1276    /**
1277     * Close a table header
1278     */
1279    function tablethead_close() {
1280        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1281    }
1282
1283    /**
1284     * Open a table row
1285     */
1286    function tablerow_open() {
1287        // initialize the cell counter used for classes
1288        $this->_counter['cell_counter'] = 0;
1289        $class                          = 'row'.$this->_counter['row_counter']++;
1290        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1291    }
1292
1293    /**
1294     * Close a table row
1295     */
1296    function tablerow_close() {
1297        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1298    }
1299
1300    /**
1301     * Open a table header cell
1302     *
1303     * @param int    $colspan
1304     * @param string $align left|center|right
1305     * @param int    $rowspan
1306     */
1307    function tableheader_open($colspan = 1, $align = null, $rowspan = 1) {
1308        $class = 'class="col'.$this->_counter['cell_counter']++;
1309        if(!is_null($align)) {
1310            $class .= ' '.$align.'align';
1311        }
1312        $class .= '"';
1313        $this->doc .= '<th '.$class;
1314        if($colspan > 1) {
1315            $this->_counter['cell_counter'] += $colspan - 1;
1316            $this->doc .= ' colspan="'.$colspan.'"';
1317        }
1318        if($rowspan > 1) {
1319            $this->doc .= ' rowspan="'.$rowspan.'"';
1320        }
1321        $this->doc .= '>';
1322    }
1323
1324    /**
1325     * Close a table header cell
1326     */
1327    function tableheader_close() {
1328        $this->doc .= '</th>';
1329    }
1330
1331    /**
1332     * Open a table cell
1333     *
1334     * @param int    $colspan
1335     * @param string $align left|center|right
1336     * @param int    $rowspan
1337     */
1338    function tablecell_open($colspan = 1, $align = null, $rowspan = 1) {
1339        $class = 'class="col'.$this->_counter['cell_counter']++;
1340        if(!is_null($align)) {
1341            $class .= ' '.$align.'align';
1342        }
1343        $class .= '"';
1344        $this->doc .= '<td '.$class;
1345        if($colspan > 1) {
1346            $this->_counter['cell_counter'] += $colspan - 1;
1347            $this->doc .= ' colspan="'.$colspan.'"';
1348        }
1349        if($rowspan > 1) {
1350            $this->doc .= ' rowspan="'.$rowspan.'"';
1351        }
1352        $this->doc .= '>';
1353    }
1354
1355    /**
1356     * Close a table cell
1357     */
1358    function tablecell_close() {
1359        $this->doc .= '</td>';
1360    }
1361
1362    #region Utility functions
1363
1364    /**
1365     * Build a link
1366     *
1367     * Assembles all parts defined in $link returns HTML for the link
1368     *
1369     * @author Andreas Gohr <andi@splitbrain.org>
1370     */
1371    function _formatLink($link) {
1372        //make sure the url is XHTML compliant (skip mailto)
1373        if(substr($link['url'], 0, 7) != 'mailto:') {
1374            $link['url'] = str_replace('&', '&amp;', $link['url']);
1375            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1376        }
1377        //remove double encodings in titles
1378        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1379
1380        // be sure there are no bad chars in url or title
1381        // (we can't do this for name because it can contain an img tag)
1382        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1383        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1384
1385        $ret = '';
1386        $ret .= $link['pre'];
1387        $ret .= '<a href="'.$link['url'].'"';
1388        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1389        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1390        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1391        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1392        if(!empty($link['rel'])) $ret .= ' rel="'.$link['rel'].'"';
1393        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1394        $ret .= '>';
1395        $ret .= $link['name'];
1396        $ret .= '</a>';
1397        $ret .= $link['suf'];
1398        return $ret;
1399    }
1400
1401    /**
1402     * Renders internal and external media
1403     *
1404     * @author Andreas Gohr <andi@splitbrain.org>
1405     * @param string $src       media ID
1406     * @param string $title     descriptive text
1407     * @param string $align     left|center|right
1408     * @param int    $width     width of media in pixel
1409     * @param int    $height    height of media in pixel
1410     * @param string $cache     cache|recache|nocache
1411     * @param bool   $render    should the media be embedded inline or just linked
1412     * @return string
1413     */
1414    function _media($src, $title = null, $align = null, $width = null,
1415                    $height = null, $cache = null, $render = true) {
1416
1417        $ret = '';
1418
1419        list($ext, $mime) = mimetype($src);
1420        if(substr($mime, 0, 5) == 'image') {
1421            // first get the $title
1422            if(!is_null($title)) {
1423                $title = $this->_xmlEntities($title);
1424            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1425                //try to use the caption from IPTC/EXIF
1426                require_once(DOKU_INC.'inc/JpegMeta.php');
1427                $jpeg = new JpegMeta(mediaFN($src));
1428                if($jpeg !== false) $cap = $jpeg->getTitle();
1429                if(!empty($cap)) {
1430                    $title = $this->_xmlEntities($cap);
1431                }
1432            }
1433            if(!$render) {
1434                // if the picture is not supposed to be rendered
1435                // return the title of the picture
1436                if(!$title) {
1437                    // just show the sourcename
1438                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1439                }
1440                return $title;
1441            }
1442            //add image tag
1443            $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"';
1444            $ret .= ' class="media'.$align.'"';
1445
1446            if($title) {
1447                $ret .= ' title="'.$title.'"';
1448                $ret .= ' alt="'.$title.'"';
1449            } else {
1450                $ret .= ' alt=""';
1451            }
1452
1453            if(!is_null($width))
1454                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1455
1456            if(!is_null($height))
1457                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1458
1459            $ret .= ' />';
1460
1461        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1462            // first get the $title
1463            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1464            if(!$render) {
1465                // if the file is not supposed to be rendered
1466                // return the title of the file (just the sourcename if there is no title)
1467                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1468            }
1469
1470            $att          = array();
1471            $att['class'] = "media$align";
1472            if($title) {
1473                $att['title'] = $title;
1474            }
1475
1476            if(media_supportedav($mime, 'video')) {
1477                //add video
1478                $ret .= $this->_video($src, $width, $height, $att);
1479            }
1480            if(media_supportedav($mime, 'audio')) {
1481                //add audio
1482                $ret .= $this->_audio($src, $att);
1483            }
1484
1485        } elseif($mime == 'application/x-shockwave-flash') {
1486            if(!$render) {
1487                // if the flash is not supposed to be rendered
1488                // return the title of the flash
1489                if(!$title) {
1490                    // just show the sourcename
1491                    $title = utf8_basename(noNS($src));
1492                }
1493                return $this->_xmlEntities($title);
1494            }
1495
1496            $att          = array();
1497            $att['class'] = "media$align";
1498            if($align == 'right') $att['align'] = 'right';
1499            if($align == 'left') $att['align'] = 'left';
1500            $ret .= html_flashobject(
1501                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1502                array('quality' => 'high'),
1503                null,
1504                $att,
1505                $this->_xmlEntities($title)
1506            );
1507        } elseif($title) {
1508            // well at least we have a title to display
1509            $ret .= $this->_xmlEntities($title);
1510        } else {
1511            // just show the sourcename
1512            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1513        }
1514
1515        return $ret;
1516    }
1517
1518    /**
1519     * Escape string for output
1520     *
1521     * @param $string
1522     * @return string
1523     */
1524    function _xmlEntities($string) {
1525        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1526    }
1527
1528    /**
1529     * Creates a linkid from a headline
1530     *
1531     * @author Andreas Gohr <andi@splitbrain.org>
1532     * @param string  $title   The headline title
1533     * @param boolean $create  Create a new unique ID?
1534     * @return string
1535     */
1536    function _headerToLink($title, $create = false) {
1537        if($create) {
1538            return sectionID($title, $this->headers);
1539        } else {
1540            $check = false;
1541            return sectionID($title, $check);
1542        }
1543    }
1544
1545    /**
1546     * Construct a title and handle images in titles
1547     *
1548     * @author Harry Fuecks <hfuecks@gmail.com>
1549     * @param string|array $title    either string title or media array
1550     * @param string       $default  default title if nothing else is found
1551     * @param bool         $isImage  will be set to true if it's a media file
1552     * @param null|string  $id       linked page id (used to extract title from first heading)
1553     * @param string       $linktype content|navigation
1554     * @return string      HTML of the title, might be full image tag or just escaped text
1555     */
1556    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1557        $isImage = false;
1558        if(is_array($title)) {
1559            $isImage = true;
1560            return $this->_imageTitle($title);
1561        } elseif(is_null($title) || trim($title) == '') {
1562            if(useHeading($linktype) && $id) {
1563                $heading = p_get_first_heading($id);
1564                if($heading) {
1565                    return $this->_xmlEntities($heading);
1566                }
1567            }
1568            return $this->_xmlEntities($default);
1569        } else {
1570            return $this->_xmlEntities($title);
1571        }
1572    }
1573
1574    /**
1575     * Returns HTML code for images used in link titles
1576     *
1577     * @author Andreas Gohr <andi@splitbrain.org>
1578     * @param string $img
1579     * @return string HTML img tag or similar
1580     */
1581    function _imageTitle($img) {
1582        global $ID;
1583
1584        // some fixes on $img['src']
1585        // see internalmedia() and externalmedia()
1586        list($img['src']) = explode('#', $img['src'], 2);
1587        if($img['type'] == 'internalmedia') {
1588            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
1589        }
1590
1591        return $this->_media(
1592            $img['src'],
1593            $img['title'],
1594            $img['align'],
1595            $img['width'],
1596            $img['height'],
1597            $img['cache']
1598        );
1599    }
1600
1601    /**
1602     * helperfunction to return a basic link to a media
1603     *
1604     * used in internalmedia() and externalmedia()
1605     *
1606     * @author   Pierre Spring <pierre.spring@liip.ch>
1607     * @param string $src       media ID
1608     * @param string $title     descriptive text
1609     * @param string $align     left|center|right
1610     * @param int    $width     width of media in pixel
1611     * @param int    $height    height of media in pixel
1612     * @param string $cache     cache|recache|nocache
1613     * @param bool   $render    should the media be embedded inline or just linked
1614     * @return array associative array with link config
1615     */
1616    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1617        global $conf;
1618
1619        $link           = array();
1620        $link['class']  = 'media';
1621        $link['style']  = '';
1622        $link['pre']    = '';
1623        $link['suf']    = '';
1624        $link['more']   = '';
1625        $link['target'] = $conf['target']['media'];
1626        $link['title']  = $this->_xmlEntities($src);
1627        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1628
1629        return $link;
1630    }
1631
1632    /**
1633     * Embed video(s) in HTML
1634     *
1635     * @author Anika Henke <anika@selfthinker.org>
1636     *
1637     * @param string $src         - ID of video to embed
1638     * @param int    $width       - width of the video in pixels
1639     * @param int    $height      - height of the video in pixels
1640     * @param array  $atts        - additional attributes for the <video> tag
1641     * @return string
1642     */
1643    function _video($src, $width, $height, $atts = null) {
1644        // prepare width and height
1645        if(is_null($atts)) $atts = array();
1646        $atts['width']  = (int) $width;
1647        $atts['height'] = (int) $height;
1648        if(!$atts['width']) $atts['width'] = 320;
1649        if(!$atts['height']) $atts['height'] = 240;
1650
1651        $posterUrl = '';
1652        $files = array();
1653        $isExternal = media_isexternal($src);
1654
1655        if ($isExternal) {
1656            // take direct source for external files
1657            list(/*ext*/, $srcMime) = mimetype($src);
1658            $files[$srcMime] = $src;
1659        } else {
1660            // prepare alternative formats
1661            $extensions   = array('webm', 'ogv', 'mp4');
1662            $files        = media_alternativefiles($src, $extensions);
1663            $poster       = media_alternativefiles($src, array('jpg', 'png'), true);
1664            if(!empty($poster)) {
1665                $posterUrl = ml(reset($poster), '', true, '&');
1666            }
1667        }
1668
1669        $out = '';
1670        // open video tag
1671        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1672        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1673        $out .= '>'.NL;
1674        $fallback = '';
1675
1676        // output source for each alternative video format
1677        foreach($files as $mime => $file) {
1678            if ($isExternal) {
1679                $url = $file;
1680                $linkType = 'externalmedia';
1681            } else {
1682                $url = ml($file, '', true, '&');
1683                $linkType = 'internalmedia';
1684            }
1685            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1686
1687            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1688            // alternative content (just a link to the file)
1689            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1690        }
1691
1692        // finish
1693        $out .= $fallback;
1694        $out .= '</video>'.NL;
1695        return $out;
1696    }
1697
1698    /**
1699     * Embed audio in HTML
1700     *
1701     * @author Anika Henke <anika@selfthinker.org>
1702     *
1703     * @param string $src       - ID of audio to embed
1704     * @param array  $atts      - additional attributes for the <audio> tag
1705     * @return string
1706     */
1707    function _audio($src, $atts = null) {
1708        $files = array();
1709        $isExternal = media_isexternal($src);
1710
1711        if ($isExternal) {
1712            // take direct source for external files
1713            list(/*ext*/, $srcMime) = mimetype($src);
1714            $files[$srcMime] = $src;
1715        } else {
1716            // prepare alternative formats
1717            $extensions   = array('ogg', 'mp3', 'wav');
1718            $files        = media_alternativefiles($src, $extensions);
1719        }
1720
1721        $out = '';
1722        // open audio tag
1723        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1724        $fallback = '';
1725
1726        // output source for each alternative audio format
1727        foreach($files as $mime => $file) {
1728            if ($isExternal) {
1729                $url = $file;
1730                $linkType = 'externalmedia';
1731            } else {
1732                $url = ml($file, '', true, '&');
1733                $linkType = 'internalmedia';
1734            }
1735            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1736
1737            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1738            // alternative content (just a link to the file)
1739            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1740        }
1741
1742        // finish
1743        $out .= $fallback;
1744        $out .= '</audio>'.NL;
1745        return $out;
1746    }
1747
1748    /**
1749     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1750     * which returns an existing media revision less or equal to rev or date_at
1751     *
1752     * @author lisps
1753     * @param string $media_id
1754     * @access protected
1755     * @return string revision ('' for current)
1756     */
1757    function _getLastMediaRevisionAt($media_id){
1758        if(!$this->date_at || media_isexternal($media_id)) return '';
1759        $pagelog = new MediaChangeLog($media_id);
1760        return $pagelog->getLastRevisionAt($this->date_at);
1761    }
1762
1763    #endregion
1764}
1765
1766//Setup VIM: ex: et ts=4 :
1767