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