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