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