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