Failed Conditions
Push — seceditfallback ( ac025f )
by Andreas
06:24 queued 03:32
created

inc/parser/xhtml.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Renderer for XHTML output
4
 *
5
 * @author Harry Fuecks <[email protected]>
6
 * @author Andreas Gohr <[email protected]>
7
 */
8
if(!defined('DOKU_INC')) die('meh.');
9
10
if(!defined('DOKU_LF')) {
11
    // Some whitespace to help View > Source
12
    define ('DOKU_LF', "\n");
13
}
14
15
if(!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
 */
25
class 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 int    $start  The byte position for the edit start
63
     * @param array  $data   Associative array with section data:
64
     *                       Key 'name': the section name/title
65
     *                       Key 'target': the target for the section edit,
66
     *                                     e.g. 'section' or 'table'
67
     *                       Key 'hid': header id
68
     *                       Key 'codeblockOffset': actual code block index
69
     *                       Key 'start': set in startSectionEdit(),
70
     *                                    do not set yourself
71
     *                       Key 'range': calculated from 'start' and
72
     *                                    $key in finishSectionEdit(),
73
     *                                    do not set yourself
74
     * @return string  A marker class for the starting HTML element
75
     *
76
     * @author Adrian Lang <[email protected]>
77
     */
78
    public function startSectionEdit($start, $data) {
79
        if (!is_array($data)) {
80
            msg(
81
                sprintf(
82
                    'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
83
                    hsc((string) $data)
84
                ), -1
85
            );
86
87
            // @deprecated 2018-04-14, backward compatibility
88
            $args = func_get_args();
89
            $data = array();
90
            if(isset($args[1])) $data['target'] = $args[1];
91
            if(isset($args[2])) $data['name'] = $args[2];
92
            if(isset($args[3])) $data['hid'] = $args[3];
93
        }
94
        $data['secid'] = ++$this->lastsecid;
95
        $data['start'] = $start;
96
        $this->sectionedits[] = $data;
97
        return 'sectionedit'.$data['secid'];
98
    }
99
100
    /**
101
     * Finish an edit section range
102
     *
103
     * @param int  $end     The byte position for the edit end; null for the rest of the page
104
     *
105
     * @author Adrian Lang <[email protected]>
106
     */
107
    public function finishSectionEdit($end = null, $hid = null) {
108
        $data = array_pop($this->sectionedits);
109
        if(!is_null($end) && $end <= $data['start']) {
110
            return;
111
        }
112
        if(!is_null($hid)) {
113
            $data['hid'] .= $hid;
114
        }
115
        $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end);
116
        unset($data['start']);
117
        $this->doc .= '<!-- EDIT'.json_encode ($data).' -->';
118
    }
119
120
    /**
121
     * Returns the format produced by this renderer.
122
     *
123
     * @return string always 'xhtml'
124
     */
125
    function getFormat() {
126
        return 'xhtml';
127
    }
128
129
    /**
130
     * Initialize the document
131
     */
132
    function document_start() {
133
        //reset some internals
134
        $this->toc     = array();
135
        $this->headers = array();
136
    }
137
138
    /**
139
     * Finalize the document
140
     */
141
    function document_end() {
142
        // Finish open section edits.
143
        while(count($this->sectionedits) > 0) {
144
            if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
145
                // If there is only one section, do not write a section edit
146
                // marker.
147
                array_pop($this->sectionedits);
148
            } else {
149
                $this->finishSectionEdit();
150
            }
151
        }
152
153
        if(count($this->footnotes) > 0) {
154
            $this->doc .= '<div class="footnotes">'.DOKU_LF;
155
156
            foreach($this->footnotes as $id => $footnote) {
157
                // check its not a placeholder that indicates actual footnote text is elsewhere
158
                if(substr($footnote, 0, 5) != "@@FNT") {
159
160
                    // open the footnote and set the anchor and backlink
161
                    $this->doc .= '<div class="fn">';
162
                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
163
                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
164
165
                    // get any other footnotes that use the same markup
166
                    $alt = array_keys($this->footnotes, "@@FNT$id");
167
168
                    if(count($alt)) {
169
                        foreach($alt as $ref) {
170
                            // set anchor and backlink for the other footnotes
171
                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
172
                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
173
                        }
174
                    }
175
176
                    // add footnote markup and close this footnote
177
                    $this->doc .= '<div class="content">'.$footnote.'</div>';
178
                    $this->doc .= '</div>'.DOKU_LF;
179
                }
180
            }
181
            $this->doc .= '</div>'.DOKU_LF;
182
        }
183
184
        // Prepare the TOC
185
        global $conf;
186
        if($this->info['toc'] && is_array($this->toc) && $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']) {
187
            global $TOC;
188
            $TOC = $this->toc;
189
        }
190
191
        // make sure there are no empty paragraphs
192
        $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
193
    }
194
195
    /**
196
     * Add an item to the TOC
197
     *
198
     * @param string $id       the hash link
199
     * @param string $text     the text to display
200
     * @param int    $level    the nesting level
201
     */
202
    function toc_additem($id, $text, $level) {
203
        global $conf;
204
205
        //handle TOC
206
        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
207
            $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
208
        }
209
    }
210
211
    /**
212
     * Render a heading
213
     *
214
     * @param string $text  the text to display
215
     * @param int    $level header level
216
     * @param int    $pos   byte position in the original source
217
     */
218
    function header($text, $level, $pos) {
219
        global $conf;
220
221
        if(blank($text)) return; //skip empty headlines
222
223
        $hid = $this->_headerToLink($text, true);
224
225
        //only add items within configured levels
226
        $this->toc_additem($hid, $text, $level);
227
228
        // adjust $node to reflect hierarchy of levels
229
        $this->node[$level - 1]++;
230
        if($level < $this->lastlevel) {
231
            for($i = 0; $i < $this->lastlevel - $level; $i++) {
232
                $this->node[$this->lastlevel - $i - 1] = 0;
233
            }
234
        }
235
        $this->lastlevel = $level;
236
237
        if($level <= $conf['maxseclevel'] &&
238
            count($this->sectionedits) > 0 &&
239
            $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
240
        ) {
241
            $this->finishSectionEdit($pos - 1);
242
        }
243
244
        // write the header
245
        $this->doc .= DOKU_LF.'<h'.$level;
246
        if($level <= $conf['maxseclevel']) {
247
            $data = array();
248
            $data['target'] = 'section';
249
            $data['name'] = $text;
250
            $data['hid'] = $hid;
251
            $data['codeblockOffset'] = $this->_codeblock;
252
            $this->doc .= ' class="'.$this->startSectionEdit($pos, $data).'"';
253
        }
254
        $this->doc .= ' id="'.$hid.'">';
255
        $this->doc .= $this->_xmlEntities($text);
256
        $this->doc .= "</h$level>".DOKU_LF;
257
    }
258
259
    /**
260
     * Open a new section
261
     *
262
     * @param int $level section level (as determined by the previous header)
263
     */
264
    function section_open($level) {
265
        $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
266
    }
267
268
    /**
269
     * Close the current section
270
     */
271
    function section_close() {
272
        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
273
    }
274
275
    /**
276
     * Render plain text data
277
     *
278
     * @param $text
279
     */
280
    function cdata($text) {
281
        $this->doc .= $this->_xmlEntities($text);
282
    }
283
284
    /**
285
     * Open a paragraph
286
     */
287
    function p_open() {
288
        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
289
    }
290
291
    /**
292
     * Close a paragraph
293
     */
294
    function p_close() {
295
        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
296
    }
297
298
    /**
299
     * Create a line break
300
     */
301
    function linebreak() {
302
        $this->doc .= '<br/>'.DOKU_LF;
303
    }
304
305
    /**
306
     * Create a horizontal line
307
     */
308
    function hr() {
309
        $this->doc .= '<hr />'.DOKU_LF;
310
    }
311
312
    /**
313
     * Start strong (bold) formatting
314
     */
315
    function strong_open() {
316
        $this->doc .= '<strong>';
317
    }
318
319
    /**
320
     * Stop strong (bold) formatting
321
     */
322
    function strong_close() {
323
        $this->doc .= '</strong>';
324
    }
325
326
    /**
327
     * Start emphasis (italics) formatting
328
     */
329
    function emphasis_open() {
330
        $this->doc .= '<em>';
331
    }
332
333
    /**
334
     * Stop emphasis (italics) formatting
335
     */
336
    function emphasis_close() {
337
        $this->doc .= '</em>';
338
    }
339
340
    /**
341
     * Start underline formatting
342
     */
343
    function underline_open() {
344
        $this->doc .= '<em class="u">';
345
    }
346
347
    /**
348
     * Stop underline formatting
349
     */
350
    function underline_close() {
351
        $this->doc .= '</em>';
352
    }
353
354
    /**
355
     * Start monospace formatting
356
     */
357
    function monospace_open() {
358
        $this->doc .= '<code>';
359
    }
360
361
    /**
362
     * Stop monospace formatting
363
     */
364
    function monospace_close() {
365
        $this->doc .= '</code>';
366
    }
367
368
    /**
369
     * Start a subscript
370
     */
371
    function subscript_open() {
372
        $this->doc .= '<sub>';
373
    }
374
375
    /**
376
     * Stop a subscript
377
     */
378
    function subscript_close() {
379
        $this->doc .= '</sub>';
380
    }
381
382
    /**
383
     * Start a superscript
384
     */
385
    function superscript_open() {
386
        $this->doc .= '<sup>';
387
    }
388
389
    /**
390
     * Stop a superscript
391
     */
392
    function superscript_close() {
393
        $this->doc .= '</sup>';
394
    }
395
396
    /**
397
     * Start deleted (strike-through) formatting
398
     */
399
    function deleted_open() {
400
        $this->doc .= '<del>';
401
    }
402
403
    /**
404
     * Stop deleted (strike-through) formatting
405
     */
406
    function deleted_close() {
407
        $this->doc .= '</del>';
408
    }
409
410
    /**
411
     * Callback for footnote start syntax
412
     *
413
     * All following content will go to the footnote instead of
414
     * the document. To achieve this the previous rendered content
415
     * is moved to $store and $doc is cleared
416
     *
417
     * @author Andreas Gohr <[email protected]>
418
     */
419
    function footnote_open() {
420
421
        // move current content to store and record footnote
422
        $this->store = $this->doc;
423
        $this->doc   = '';
424
    }
425
426
    /**
427
     * Callback for footnote end syntax
428
     *
429
     * All rendered content is moved to the $footnotes array and the old
430
     * content is restored from $store again
431
     *
432
     * @author Andreas Gohr
433
     */
434
    function footnote_close() {
435
        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
436
        static $fnid = 0;
437
        // assign new footnote id (we start at 1)
438
        $fnid++;
439
440
        // recover footnote into the stack and restore old content
441
        $footnote    = $this->doc;
442
        $this->doc   = $this->store;
443
        $this->store = '';
444
445
        // check to see if this footnote has been seen before
446
        $i = array_search($footnote, $this->footnotes);
447
448
        if($i === false) {
449
            // its a new footnote, add it to the $footnotes array
450
            $this->footnotes[$fnid] = $footnote;
451
        } else {
452
            // seen this one before, save a placeholder
453
            $this->footnotes[$fnid] = "@@FNT".($i);
454
        }
455
456
        // output the footnote reference and link
457
        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
458
    }
459
460
    /**
461
     * Open an unordered list
462
     *
463
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
464
     */
465
    function listu_open($classes = null) {
466
        $class = '';
467
        if($classes !== null) {
468
            if(is_array($classes)) $classes = join(' ', $classes);
469
            $class = " class=\"$classes\"";
470
        }
471
        $this->doc .= "<ul$class>".DOKU_LF;
472
    }
473
474
    /**
475
     * Close an unordered list
476
     */
477
    function listu_close() {
478
        $this->doc .= '</ul>'.DOKU_LF;
479
    }
480
481
    /**
482
     * Open an ordered list
483
     *
484
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
485
     */
486
    function listo_open($classes = null) {
0 ignored issues
show
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
487
        $class = '';
488
        if($classes !== null) {
489
            if(is_array($classes)) $classes = join(' ', $classes);
490
            $class = " class=\"$classes\"";
491
        }
492
        $this->doc .= "<ol$class>".DOKU_LF;
493
    }
494
495
    /**
496
     * Close an ordered list
497
     */
498
    function listo_close() {
499
        $this->doc .= '</ol>'.DOKU_LF;
500
    }
501
502
    /**
503
     * Open a list item
504
     *
505
     * @param int $level the nesting level
506
     * @param bool $node true when a node; false when a leaf
507
     */
508
    function listitem_open($level, $node=false) {
509
        $branching = $node ? ' node' : '';
510
        $this->doc .= '<li class="level'.$level.$branching.'">';
511
    }
512
513
    /**
514
     * Close a list item
515
     */
516
    function listitem_close() {
517
        $this->doc .= '</li>'.DOKU_LF;
518
    }
519
520
    /**
521
     * Start the content of a list item
522
     */
523
    function listcontent_open() {
524
        $this->doc .= '<div class="li">';
525
    }
526
527
    /**
528
     * Stop the content of a list item
529
     */
530
    function listcontent_close() {
531
        $this->doc .= '</div>'.DOKU_LF;
532
    }
533
534
    /**
535
     * Output unformatted $text
536
     *
537
     * Defaults to $this->cdata()
538
     *
539
     * @param string $text
540
     */
541
    function unformatted($text) {
542
        $this->doc .= $this->_xmlEntities($text);
543
    }
544
545
    /**
546
     * Execute PHP code if allowed
547
     *
548
     * @param  string $text      PHP code that is either executed or printed
549
     * @param  string $wrapper   html element to wrap result if $conf['phpok'] is okff
550
     *
551
     * @author Andreas Gohr <[email protected]>
552
     */
553
    function php($text, $wrapper = 'code') {
554
        global $conf;
555
556
        if($conf['phpok']) {
557
            ob_start();
558
            eval($text);
559
            $this->doc .= ob_get_contents();
560
            ob_end_clean();
561
        } else {
562
            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
563
        }
564
    }
565
566
    /**
567
     * Output block level PHP code
568
     *
569
     * If $conf['phpok'] is true this should evaluate the given code and append the result
570
     * to $doc
571
     *
572
     * @param string $text The PHP code
573
     */
574
    function phpblock($text) {
575
        $this->php($text, 'pre');
576
    }
577
578
    /**
579
     * Insert HTML if allowed
580
     *
581
     * @param  string $text      html text
582
     * @param  string $wrapper   html element to wrap result if $conf['htmlok'] is okff
583
     *
584
     * @author Andreas Gohr <[email protected]>
585
     */
586
    function html($text, $wrapper = 'code') {
587
        global $conf;
588
589
        if($conf['htmlok']) {
590
            $this->doc .= $text;
591
        } else {
592
            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
593
        }
594
    }
595
596
    /**
597
     * Output raw block-level HTML
598
     *
599
     * If $conf['htmlok'] is true this should add the code as is to $doc
600
     *
601
     * @param string $text The HTML
602
     */
603
    function htmlblock($text) {
604
        $this->html($text, 'pre');
605
    }
606
607
    /**
608
     * Start a block quote
609
     */
610
    function quote_open() {
611
        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
612
    }
613
614
    /**
615
     * Stop a block quote
616
     */
617
    function quote_close() {
618
        $this->doc .= '</div></blockquote>'.DOKU_LF;
619
    }
620
621
    /**
622
     * Output preformatted text
623
     *
624
     * @param string $text
625
     */
626
    function preformatted($text) {
627
        $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
628
    }
629
630
    /**
631
     * Display text as file content, optionally syntax highlighted
632
     *
633
     * @param string $text     text to show
634
     * @param string $language programming language to use for syntax highlighting
635
     * @param string $filename file path label
636
     */
637
    function file($text, $language = null, $filename = null) {
638
        $this->_highlight('file', $text, $language, $filename);
639
    }
640
641
    /**
642
     * Display text as code content, optionally syntax highlighted
643
     *
644
     * @param string $text     text to show
645
     * @param string $language programming language to use for syntax highlighting
646
     * @param string $filename file path label
647
     */
648
    function code($text, $language = null, $filename = null) {
649
        $this->_highlight('code', $text, $language, $filename);
650
    }
651
652
    /**
653
     * Use GeSHi to highlight language syntax in code and file blocks
654
     *
655
     * @author Andreas Gohr <[email protected]>
656
     * @param string $type     code|file
657
     * @param string $text     text to show
658
     * @param string $language programming language to use for syntax highlighting
659
     * @param string $filename file path label
660
     */
661
    function _highlight($type, $text, $language = null, $filename = null) {
662
        global $ID;
663
        global $lang;
664
        global $INPUT;
665
666
        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
667
668
        if($filename) {
669
            // add icon
670
            list($ext) = mimetype($filename, false);
671
            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
672
            $class = 'mediafile mf_'.$class;
673
674
            $offset = 0;
675
            if ($INPUT->has('codeblockOffset')) {
676
                $offset = $INPUT->str('codeblockOffset');
677
            }
678
            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
679
            $this->doc .= '<dt><a href="'.exportlink($ID, 'code', array('codeblock' => $offset+$this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
680
            $this->doc .= hsc($filename);
681
            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
682
        }
683
684
        if($text{0} == "\n") {
685
            $text = substr($text, 1);
686
        }
687
        if(substr($text, -1) == "\n") {
688
            $text = substr($text, 0, -1);
689
        }
690
691
        if(empty($language)) { // empty is faster than is_null and can prevent '' string
692
            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
693
        } else {
694
            $class = 'code'; //we always need the code class to make the syntax highlighting apply
695
            if($type != 'code') $class .= ' '.$type;
696
697
            $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '').'</pre>'.DOKU_LF;
698
        }
699
700
        if($filename) {
701
            $this->doc .= '</dd></dl>'.DOKU_LF;
702
        }
703
704
        $this->_codeblock++;
705
    }
706
707
    /**
708
     * Format an acronym
709
     *
710
     * Uses $this->acronyms
711
     *
712
     * @param string $acronym
713
     */
714
    function acronym($acronym) {
715
716
        if(array_key_exists($acronym, $this->acronyms)) {
717
718
            $title = $this->_xmlEntities($this->acronyms[$acronym]);
719
720
            $this->doc .= '<abbr title="'.$title
721
                .'">'.$this->_xmlEntities($acronym).'</abbr>';
722
723
        } else {
724
            $this->doc .= $this->_xmlEntities($acronym);
725
        }
726
    }
727
728
    /**
729
     * Format a smiley
730
     *
731
     * Uses $this->smiley
732
     *
733
     * @param string $smiley
734
     */
735
    function smiley($smiley) {
736
        if(array_key_exists($smiley, $this->smileys)) {
737
            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
738
                '" class="icon" alt="'.
739
                $this->_xmlEntities($smiley).'" />';
740
        } else {
741
            $this->doc .= $this->_xmlEntities($smiley);
742
        }
743
    }
744
745
    /**
746
     * Format an entity
747
     *
748
     * Entities are basically small text replacements
749
     *
750
     * Uses $this->entities
751
     *
752
     * @param string $entity
753
     */
754
    function entity($entity) {
755
        if(array_key_exists($entity, $this->entities)) {
756
            $this->doc .= $this->entities[$entity];
757
        } else {
758
            $this->doc .= $this->_xmlEntities($entity);
759
        }
760
    }
761
762
    /**
763
     * Typographically format a multiply sign
764
     *
765
     * Example: ($x=640, $y=480) should result in "640×480"
766
     *
767
     * @param string|int $x first value
768
     * @param string|int $y second value
769
     */
770
    function multiplyentity($x, $y) {
771
        $this->doc .= "$x&times;$y";
772
    }
773
774
    /**
775
     * Render an opening single quote char (language specific)
776
     */
777
    function singlequoteopening() {
778
        global $lang;
779
        $this->doc .= $lang['singlequoteopening'];
780
    }
781
782
    /**
783
     * Render a closing single quote char (language specific)
784
     */
785
    function singlequoteclosing() {
786
        global $lang;
787
        $this->doc .= $lang['singlequoteclosing'];
788
    }
789
790
    /**
791
     * Render an apostrophe char (language specific)
792
     */
793
    function apostrophe() {
794
        global $lang;
795
        $this->doc .= $lang['apostrophe'];
796
    }
797
798
    /**
799
     * Render an opening double quote char (language specific)
800
     */
801
    function doublequoteopening() {
802
        global $lang;
803
        $this->doc .= $lang['doublequoteopening'];
804
    }
805
806
    /**
807
     * Render an closinging double quote char (language specific)
808
     */
809
    function doublequoteclosing() {
810
        global $lang;
811
        $this->doc .= $lang['doublequoteclosing'];
812
    }
813
814
    /**
815
     * Render a CamelCase link
816
     *
817
     * @param string $link       The link name
818
     * @param bool   $returnonly whether to return html or write to doc attribute
819
     * @return void|string writes to doc attribute or returns html depends on $returnonly
820
     *
821
     * @see http://en.wikipedia.org/wiki/CamelCase
822
     */
823
    function camelcaselink($link, $returnonly = false) {
824
        if($returnonly) {
825
          return $this->internallink($link, $link, null, true);
826
        } else {
827
          $this->internallink($link, $link);
828
        }
829
    }
830
831
    /**
832
     * Render a page local link
833
     *
834
     * @param string $hash       hash link identifier
835
     * @param string $name       name for the link
836
     * @param bool   $returnonly whether to return html or write to doc attribute
837
     * @return void|string writes to doc attribute or returns html depends on $returnonly
838
     */
839
    function locallink($hash, $name = null, $returnonly = false) {
840
        global $ID;
841
        $name  = $this->_getLinkTitle($name, $hash, $isImage);
842
        $hash  = $this->_headerToLink($hash);
843
        $title = $ID.' ↵';
844
845
        $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
846
        $doc .= $name;
847
        $doc .= '</a>';
848
849
        if($returnonly) {
850
          return $doc;
851
        } else {
852
          $this->doc .= $doc;
853
        }
854
    }
855
856
    /**
857
     * Render an internal Wiki Link
858
     *
859
     * $search,$returnonly & $linktype are not for the renderer but are used
860
     * elsewhere - no need to implement them in other renderers
861
     *
862
     * @author Andreas Gohr <[email protected]>
863
     * @param string      $id         pageid
864
     * @param string|null $name       link name
865
     * @param string|null $search     adds search url param
866
     * @param bool        $returnonly whether to return html or write to doc attribute
867
     * @param string      $linktype   type to set use of headings
868
     * @return void|string writes to doc attribute or returns html depends on $returnonly
869
     */
870
    function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
871
        global $conf;
872
        global $ID;
873
        global $INFO;
874
875
        $params = '';
876
        $parts  = explode('?', $id, 2);
877
        if(count($parts) === 2) {
878
            $id     = $parts[0];
879
            $params = $parts[1];
880
        }
881
882
        // For empty $id we need to know the current $ID
883
        // We need this check because _simpleTitle needs
884
        // correct $id and resolve_pageid() use cleanID($id)
885
        // (some things could be lost)
886
        if($id === '') {
887
            $id = $ID;
888
        }
889
890
        // default name is based on $id as given
891
        $default = $this->_simpleTitle($id);
892
893
        // now first resolve and clean up the $id
894
        resolve_pageid(getNS($ID), $id, $exists, $this->date_at, true);
895
896
        $link = array();
897
        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
898
        if(!$isImage) {
899
            if($exists) {
900
                $class = 'wikilink1';
901
            } else {
902
                $class       = 'wikilink2';
903
                $link['rel'] = 'nofollow';
904
            }
905
        } else {
906
            $class = 'media';
907
        }
908
909
        //keep hash anchor
910
        @list($id, $hash) = explode('#', $id, 2);
911
        if(!empty($hash)) $hash = $this->_headerToLink($hash);
912
913
        //prepare for formating
914
        $link['target'] = $conf['target']['wiki'];
915
        $link['style']  = '';
916
        $link['pre']    = '';
917
        $link['suf']    = '';
918
        // highlight link to current page
919
        if($id == $INFO['id']) {
920
            $link['pre'] = '<span class="curid">';
921
            $link['suf'] = '</span>';
922
        }
923
        $link['more']   = '';
924
        $link['class']  = $class;
925
        if($this->date_at) {
926
            $params = $params.'&at='.rawurlencode($this->date_at);
927
        }
928
        $link['url']    = wl($id, $params);
929
        $link['name']   = $name;
930
        $link['title']  = $id;
931
        //add search string
932
        if($search) {
933
            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
934
            if(is_array($search)) {
935
                $search = array_map('rawurlencode', $search);
936
                $link['url'] .= 's[]='.join('&amp;s[]=', $search);
937
            } else {
938
                $link['url'] .= 's='.rawurlencode($search);
939
            }
940
        }
941
942
        //keep hash
943
        if($hash) $link['url'] .= '#'.$hash;
944
945
        //output formatted
946
        if($returnonly) {
947
            return $this->_formatLink($link);
948
        } else {
949
            $this->doc .= $this->_formatLink($link);
950
        }
951
    }
952
953
    /**
954
     * Render an external link
955
     *
956
     * @param string       $url        full URL with scheme
957
     * @param string|array $name       name for the link, array for media file
958
     * @param bool         $returnonly whether to return html or write to doc attribute
959
     * @return void|string writes to doc attribute or returns html depends on $returnonly
960
     */
961
    function externallink($url, $name = null, $returnonly = false) {
962
        global $conf;
963
964
        $name = $this->_getLinkTitle($name, $url, $isImage);
965
966
        // url might be an attack vector, only allow registered protocols
967
        if(is_null($this->schemes)) $this->schemes = getSchemes();
968
        list($scheme) = explode('://', $url);
969
        $scheme = strtolower($scheme);
970
        if(!in_array($scheme, $this->schemes)) $url = '';
971
972
        // is there still an URL?
973
        if(!$url) {
974
            if($returnonly) {
975
                return $name;
976
            } else {
977
                $this->doc .= $name;
978
            }
979
            return;
980
        }
981
982
        // set class
983
        if(!$isImage) {
984
            $class = 'urlextern';
985
        } else {
986
            $class = 'media';
987
        }
988
989
        //prepare for formating
990
        $link = array();
991
        $link['target'] = $conf['target']['extern'];
992
        $link['style']  = '';
993
        $link['pre']    = '';
994
        $link['suf']    = '';
995
        $link['more']   = '';
996
        $link['class']  = $class;
997
        $link['url']    = $url;
998
        $link['rel']    = '';
999
1000
        $link['name']  = $name;
1001
        $link['title'] = $this->_xmlEntities($url);
1002
        if($conf['relnofollow']) $link['rel'] .= ' nofollow';
1003
        if($conf['target']['extern']) $link['rel'] .= ' noopener';
1004
1005
        //output formatted
1006
        if($returnonly) {
1007
            return $this->_formatLink($link);
1008
        } else {
1009
            $this->doc .= $this->_formatLink($link);
1010
        }
1011
    }
1012
1013
    /**
1014
     * Render an interwiki link
1015
     *
1016
     * You may want to use $this->_resolveInterWiki() here
1017
     *
1018
     * @param string       $match      original link - probably not much use
1019
     * @param string|array $name       name for the link, array for media file
1020
     * @param string       $wikiName   indentifier (shortcut) for the remote wiki
1021
     * @param string       $wikiUri    the fragment parsed from the original link
1022
     * @param bool         $returnonly whether to return html or write to doc attribute
1023
     * @return void|string writes to doc attribute or returns html depends on $returnonly
1024
     */
1025
    function interwikilink($match, $name = null, $wikiName, $wikiUri, $returnonly = false) {
1026
        global $conf;
1027
1028
        $link           = array();
1029
        $link['target'] = $conf['target']['interwiki'];
1030
        $link['pre']    = '';
1031
        $link['suf']    = '';
1032
        $link['more']   = '';
1033
        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
1034
        $link['rel']    = '';
1035
1036
        //get interwiki URL
1037
        $exists = null;
1038
        $url    = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
1039
1040
        if(!$isImage) {
1041
            $class         = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
1042
            $link['class'] = "interwiki iw_$class";
1043
        } else {
1044
            $link['class'] = 'media';
1045
        }
1046
1047
        //do we stay at the same server? Use local target
1048
        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
1049
            $link['target'] = $conf['target']['wiki'];
1050
        }
1051
        if($exists !== null && !$isImage) {
1052
            if($exists) {
1053
                $link['class'] .= ' wikilink1';
1054
            } else {
1055
                $link['class'] .= ' wikilink2';
1056
                $link['rel'] .= ' nofollow';
1057
            }
1058
        }
1059
        if($conf['target']['interwiki']) $link['rel'] .= ' noopener';
1060
1061
        $link['url']   = $url;
1062
        $link['title'] = htmlspecialchars($link['url']);
1063
1064
        //output formatted
1065
        if($returnonly) {
1066
            return $this->_formatLink($link);
1067
        } else {
1068
            $this->doc .= $this->_formatLink($link);
1069
        }
1070
    }
1071
1072
    /**
1073
     * Link to windows share
1074
     *
1075
     * @param string       $url        the link
1076
     * @param string|array $name       name for the link, array for media file
1077
     * @param bool         $returnonly whether to return html or write to doc attribute
1078
     * @return void|string writes to doc attribute or returns html depends on $returnonly
1079
     */
1080
    function windowssharelink($url, $name = null, $returnonly = false) {
1081
        global $conf;
1082
1083
        //simple setup
1084
        $link = array();
1085
        $link['target'] = $conf['target']['windows'];
1086
        $link['pre']    = '';
1087
        $link['suf']    = '';
1088
        $link['style']  = '';
1089
1090
        $link['name'] = $this->_getLinkTitle($name, $url, $isImage);
1091
        if(!$isImage) {
1092
            $link['class'] = 'windows';
1093
        } else {
1094
            $link['class'] = 'media';
1095
        }
1096
1097
        $link['title'] = $this->_xmlEntities($url);
1098
        $url           = str_replace('\\', '/', $url);
1099
        $url           = 'file:///'.$url;
1100
        $link['url']   = $url;
1101
1102
        //output formatted
1103
        if($returnonly) {
1104
            return $this->_formatLink($link);
1105
        } else {
1106
            $this->doc .= $this->_formatLink($link);
1107
        }
1108
    }
1109
1110
    /**
1111
     * Render a linked E-Mail Address
1112
     *
1113
     * Honors $conf['mailguard'] setting
1114
     *
1115
     * @param string       $address    Email-Address
1116
     * @param string|array $name       name for the link, array for media file
1117
     * @param bool         $returnonly whether to return html or write to doc attribute
1118
     * @return void|string writes to doc attribute or returns html depends on $returnonly
1119
     */
1120
    function emaillink($address, $name = null, $returnonly = false) {
1121
        global $conf;
1122
        //simple setup
1123
        $link           = array();
1124
        $link['target'] = '';
1125
        $link['pre']    = '';
1126
        $link['suf']    = '';
1127
        $link['style']  = '';
1128
        $link['more']   = '';
1129
1130
        $name = $this->_getLinkTitle($name, '', $isImage);
1131
        if(!$isImage) {
1132
            $link['class'] = 'mail';
1133
        } else {
1134
            $link['class'] = 'media';
1135
        }
1136
1137
        $address = $this->_xmlEntities($address);
1138
        $address = obfuscate($address);
1139
        $title   = $address;
1140
1141
        if(empty($name)) {
1142
            $name = $address;
1143
        }
1144
1145
        if($conf['mailguard'] == 'visible') $address = rawurlencode($address);
1146
1147
        $link['url']   = 'mailto:'.$address;
1148
        $link['name']  = $name;
1149
        $link['title'] = $title;
1150
1151
        //output formatted
1152
        if($returnonly) {
1153
            return $this->_formatLink($link);
1154
        } else {
1155
            $this->doc .= $this->_formatLink($link);
1156
        }
1157
    }
1158
1159
    /**
1160
     * Render an internal media file
1161
     *
1162
     * @param string $src       media ID
1163
     * @param string $title     descriptive text
1164
     * @param string $align     left|center|right
1165
     * @param int    $width     width of media in pixel
1166
     * @param int    $height    height of media in pixel
1167
     * @param string $cache     cache|recache|nocache
1168
     * @param string $linking   linkonly|detail|nolink
1169
     * @param bool   $return    return HTML instead of adding to $doc
1170
     * @return void|string writes to doc attribute or returns html depends on $return
1171
     */
1172
    function internalmedia($src, $title = null, $align = null, $width = null,
1173
                           $height = null, $cache = null, $linking = null, $return = false) {
1174
        global $ID;
1175
        if (strpos($src, '#') !== false) {
1176
            list($src, $hash) = explode('#', $src, 2);
1177
        }
1178
        resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true);
1179
1180
        $noLink = false;
1181
        $render = ($linking == 'linkonly') ? false : true;
1182
        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1183
1184
        list($ext, $mime) = mimetype($src, false);
1185
        if(substr($mime, 0, 5) == 'image' && $render) {
1186
            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src)), ($linking == 'direct'));
1187
        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1188
            // don't link movies
1189
            $noLink = true;
1190
        } else {
1191
            // add file icons
1192
            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1193
            $link['class'] .= ' mediafile mf_'.$class;
1194
            $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache , 'rev'=>$this->_getLastMediaRevisionAt($src)), true);
1195
            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
1196
        }
1197
1198
        if (!empty($hash)) $link['url'] .= '#'.$hash;
1199
1200
        //markup non existing files
1201
        if(!$exists) {
1202
            $link['class'] .= ' wikilink2';
1203
        }
1204
1205
        //output formatted
1206
        if($return) {
1207
            if($linking == 'nolink' || $noLink) return $link['name'];
1208
            else return $this->_formatLink($link);
1209
        } else {
1210
            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1211
            else $this->doc .= $this->_formatLink($link);
1212
        }
1213
    }
1214
1215
    /**
1216
     * Render an external media file
1217
     *
1218
     * @param string $src     full media URL
1219
     * @param string $title   descriptive text
1220
     * @param string $align   left|center|right
1221
     * @param int    $width   width of media in pixel
1222
     * @param int    $height  height of media in pixel
1223
     * @param string $cache   cache|recache|nocache
1224
     * @param string $linking linkonly|detail|nolink
1225
     * @param bool   $return  return HTML instead of adding to $doc
1226
     * @return void|string writes to doc attribute or returns html depends on $return
1227
     */
1228
    function externalmedia($src, $title = null, $align = null, $width = null,
1229
                           $height = null, $cache = null, $linking = null, $return = false) {
1230
        if(link_isinterwiki($src)){
1231
            list($shortcut, $reference) = explode('>', $src, 2);
1232
            $exists = null;
1233
            $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
1234
        }
1235
        list($src, $hash) = explode('#', $src, 2);
1236
        $noLink = false;
1237
        $render = ($linking == 'linkonly') ? false : true;
1238
        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1239
1240
        $link['url'] = ml($src, array('cache' => $cache));
1241
1242
        list($ext, $mime) = mimetype($src, false);
1243
        if(substr($mime, 0, 5) == 'image' && $render) {
1244
            // link only jpeg images
1245
            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
1246
        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1247
            // don't link movies
1248
            $noLink = true;
1249
        } else {
1250
            // add file icons
1251
            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1252
            $link['class'] .= ' mediafile mf_'.$class;
1253
        }
1254
1255
        if($hash) $link['url'] .= '#'.$hash;
1256
1257
        //output formatted
1258
        if($return) {
1259
            if($linking == 'nolink' || $noLink) return $link['name'];
1260
            else return $this->_formatLink($link);
1261
        } else {
1262
            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1263
            else $this->doc .= $this->_formatLink($link);
1264
        }
1265
    }
1266
1267
    /**
1268
     * Renders an RSS feed
1269
     *
1270
     * @param string $url    URL of the feed
1271
     * @param array  $params Finetuning of the output
1272
     *
1273
     * @author Andreas Gohr <[email protected]>
1274
     */
1275
    function rss($url, $params) {
1276
        global $lang;
1277
        global $conf;
1278
1279
        require_once(DOKU_INC.'inc/FeedParser.php');
1280
        $feed = new FeedParser();
1281
        $feed->set_feed_url($url);
1282
1283
        //disable warning while fetching
1284
        if(!defined('DOKU_E_LEVEL')) {
1285
            $elvl = error_reporting(E_ERROR);
1286
        }
1287
        $rc = $feed->init();
1288
        if(isset($elvl)) {
1289
            error_reporting($elvl);
1290
        }
1291
1292
        if($params['nosort']) $feed->enable_order_by_date(false);
1293
1294
        //decide on start and end
1295
        if($params['reverse']) {
1296
            $mod   = -1;
1297
            $start = $feed->get_item_quantity() - 1;
1298
            $end   = $start - ($params['max']);
1299
            $end   = ($end < -1) ? -1 : $end;
1300
        } else {
1301
            $mod   = 1;
1302
            $start = 0;
1303
            $end   = $feed->get_item_quantity();
1304
            $end   = ($end > $params['max']) ? $params['max'] : $end;
1305
        }
1306
1307
        $this->doc .= '<ul class="rss">';
1308
        if($rc) {
1309
            for($x = $start; $x != $end; $x += $mod) {
1310
                $item = $feed->get_item($x);
1311
                $this->doc .= '<li><div class="li">';
1312
                // support feeds without links
1313
                $lnkurl = $item->get_permalink();
1314
                if($lnkurl) {
1315
                    // title is escaped by SimplePie, we unescape here because it
1316
                    // is escaped again in externallink() FS#1705
1317
                    $this->externallink(
1318
                        $item->get_permalink(),
1319
                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
1320
                    );
1321
                } else {
1322
                    $this->doc .= ' '.$item->get_title();
1323
                }
1324
                if($params['author']) {
1325
                    $author = $item->get_author(0);
1326
                    if($author) {
1327
                        $name = $author->get_name();
1328
                        if(!$name) $name = $author->get_email();
1329
                        if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name);
1330
                    }
1331
                }
1332
                if($params['date']) {
1333
                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1334
                }
1335
                if($params['details']) {
1336
                    $this->doc .= '<div class="detail">';
1337
                    if($conf['htmlok']) {
1338
                        $this->doc .= $item->get_description();
1339
                    } else {
1340
                        $this->doc .= strip_tags($item->get_description());
1341
                    }
1342
                    $this->doc .= '</div>';
1343
                }
1344
1345
                $this->doc .= '</div></li>';
1346
            }
1347
        } else {
1348
            $this->doc .= '<li><div class="li">';
1349
            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1350
            $this->externallink($url);
1351
            if($conf['allowdebug']) {
1352
                $this->doc .= '<!--'.hsc($feed->error).'-->';
1353
            }
1354
            $this->doc .= '</div></li>';
1355
        }
1356
        $this->doc .= '</ul>';
1357
    }
1358
1359
    /**
1360
     * Start a table
1361
     *
1362
     * @param int $maxcols maximum number of columns
1363
     * @param int $numrows NOT IMPLEMENTED
1364
     * @param int $pos byte position in the original source
1365
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1366
     */
1367
    function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
1368
        // initialize the row counter used for classes
1369
        $this->_counter['row_counter'] = 0;
1370
        $class                         = 'table';
1371
        if($classes !== null) {
1372
            if(is_array($classes)) $classes = join(' ', $classes);
1373
            $class .= ' ' . $classes;
1374
        }
1375
        if($pos !== null) {
1376
            $hid = $this->_headerToLink($class, true);
1377
            $data = array();
1378
            $data['target'] = 'table';
1379
            $data['name'] = '';
1380
            $data['hid'] = $hid;
1381
            $class .= ' '.$this->startSectionEdit($pos, $data);
1382
        }
1383
        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1384
            DOKU_LF;
1385
    }
1386
1387
    /**
1388
     * Close a table
1389
     *
1390
     * @param int $pos byte position in the original source
1391
     */
1392
    function table_close($pos = null) {
1393
        $this->doc .= '</table></div>'.DOKU_LF;
1394
        if($pos !== null) {
1395
            $this->finishSectionEdit($pos);
1396
        }
1397
    }
1398
1399
    /**
1400
     * Open a table header
1401
     */
1402
    function tablethead_open() {
1403
        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1404
    }
1405
1406
    /**
1407
     * Close a table header
1408
     */
1409
    function tablethead_close() {
1410
        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1411
    }
1412
1413
    /**
1414
     * Open a table body
1415
     */
1416
    function tabletbody_open() {
1417
        $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
1418
    }
1419
1420
    /**
1421
     * Close a table body
1422
     */
1423
    function tabletbody_close() {
1424
        $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
1425
    }
1426
1427
    /**
1428
     * Open a table footer
1429
     */
1430
    function tabletfoot_open() {
1431
        $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
1432
    }
1433
1434
    /**
1435
     * Close a table footer
1436
     */
1437
    function tabletfoot_close() {
1438
        $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
1439
    }
1440
1441
    /**
1442
     * Open a table row
1443
     *
1444
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1445
     */
1446
    function tablerow_open($classes = null) {
1447
        // initialize the cell counter used for classes
1448
        $this->_counter['cell_counter'] = 0;
1449
        $class                          = 'row'.$this->_counter['row_counter']++;
1450
        if($classes !== null) {
1451
            if(is_array($classes)) $classes = join(' ', $classes);
1452
            $class .= ' ' . $classes;
1453
        }
1454
        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1455
    }
1456
1457
    /**
1458
     * Close a table row
1459
     */
1460
    function tablerow_close() {
1461
        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1462
    }
1463
1464
    /**
1465
     * Open a table header cell
1466
     *
1467
     * @param int    $colspan
1468
     * @param string $align left|center|right
1469
     * @param int    $rowspan
1470
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1471
     */
1472
    function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1473
        $class = 'class="col'.$this->_counter['cell_counter']++;
1474
        if(!is_null($align)) {
1475
            $class .= ' '.$align.'align';
1476
        }
1477
        if($classes !== null) {
1478
            if(is_array($classes)) $classes = join(' ', $classes);
1479
            $class .= ' ' . $classes;
1480
        }
1481
        $class .= '"';
1482
        $this->doc .= '<th '.$class;
1483
        if($colspan > 1) {
1484
            $this->_counter['cell_counter'] += $colspan - 1;
1485
            $this->doc .= ' colspan="'.$colspan.'"';
1486
        }
1487
        if($rowspan > 1) {
1488
            $this->doc .= ' rowspan="'.$rowspan.'"';
1489
        }
1490
        $this->doc .= '>';
1491
    }
1492
1493
    /**
1494
     * Close a table header cell
1495
     */
1496
    function tableheader_close() {
1497
        $this->doc .= '</th>';
1498
    }
1499
1500
    /**
1501
     * Open a table cell
1502
     *
1503
     * @param int       $colspan
1504
     * @param string    $align left|center|right
1505
     * @param int       $rowspan
1506
     * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
1507
     */
1508
    function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1509
        $class = 'class="col'.$this->_counter['cell_counter']++;
1510
        if(!is_null($align)) {
1511
            $class .= ' '.$align.'align';
1512
        }
1513
        if($classes !== null) {
1514
            if(is_array($classes)) $classes = join(' ', $classes);
1515
            $class .= ' ' . $classes;
1516
        }
1517
        $class .= '"';
1518
        $this->doc .= '<td '.$class;
1519
        if($colspan > 1) {
1520
            $this->_counter['cell_counter'] += $colspan - 1;
1521
            $this->doc .= ' colspan="'.$colspan.'"';
1522
        }
1523
        if($rowspan > 1) {
1524
            $this->doc .= ' rowspan="'.$rowspan.'"';
1525
        }
1526
        $this->doc .= '>';
1527
    }
1528
1529
    /**
1530
     * Close a table cell
1531
     */
1532
    function tablecell_close() {
1533
        $this->doc .= '</td>';
1534
    }
1535
1536
    /**
1537
     * Returns the current header level.
1538
     * (required e.g. by the filelist plugin)
1539
     *
1540
     * @return int The current header level
1541
     */
1542
    function getLastlevel() {
1543
        return $this->lastlevel;
1544
    }
1545
1546
    #region Utility functions
1547
1548
    /**
1549
     * Build a link
1550
     *
1551
     * Assembles all parts defined in $link returns HTML for the link
1552
     *
1553
     * @param array $link attributes of a link
1554
     * @return string
1555
     *
1556
     * @author Andreas Gohr <[email protected]>
1557
     */
1558
    function _formatLink($link) {
1559
        //make sure the url is XHTML compliant (skip mailto)
1560
        if(substr($link['url'], 0, 7) != 'mailto:') {
1561
            $link['url'] = str_replace('&', '&amp;', $link['url']);
1562
            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1563
        }
1564
        //remove double encodings in titles
1565
        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1566
1567
        // be sure there are no bad chars in url or title
1568
        // (we can't do this for name because it can contain an img tag)
1569
        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1570
        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1571
1572
        $ret = '';
1573
        $ret .= $link['pre'];
1574
        $ret .= '<a href="'.$link['url'].'"';
1575
        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1576
        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1577
        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1578
        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1579
        if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
1580
        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1581
        $ret .= '>';
1582
        $ret .= $link['name'];
1583
        $ret .= '</a>';
1584
        $ret .= $link['suf'];
1585
        return $ret;
1586
    }
1587
1588
    /**
1589
     * Renders internal and external media
1590
     *
1591
     * @author Andreas Gohr <[email protected]>
1592
     * @param string $src       media ID
1593
     * @param string $title     descriptive text
1594
     * @param string $align     left|center|right
1595
     * @param int    $width     width of media in pixel
1596
     * @param int    $height    height of media in pixel
1597
     * @param string $cache     cache|recache|nocache
1598
     * @param bool   $render    should the media be embedded inline or just linked
1599
     * @return string
1600
     */
1601
    function _media($src, $title = null, $align = null, $width = null,
1602
                    $height = null, $cache = null, $render = true) {
1603
1604
        $ret = '';
1605
1606
        list($ext, $mime) = mimetype($src);
1607
        if(substr($mime, 0, 5) == 'image') {
1608
            // first get the $title
1609
            if(!is_null($title)) {
1610
                $title = $this->_xmlEntities($title);
1611
            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1612
                //try to use the caption from IPTC/EXIF
1613
                require_once(DOKU_INC.'inc/JpegMeta.php');
1614
                $jpeg = new JpegMeta(mediaFN($src));
1615
                if($jpeg !== false) $cap = $jpeg->getTitle();
1616
                if(!empty($cap)) {
1617
                    $title = $this->_xmlEntities($cap);
1618
                }
1619
            }
1620
            if(!$render) {
1621
                // if the picture is not supposed to be rendered
1622
                // return the title of the picture
1623
                if(!$title) {
1624
                    // just show the sourcename
1625
                    $title = $this->_xmlEntities(utf8_basename(noNS($src)));
1626
                }
1627
                return $title;
1628
            }
1629
            //add image tag
1630
            $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"';
1631
            $ret .= ' class="media'.$align.'"';
1632
1633
            if($title) {
1634
                $ret .= ' title="'.$title.'"';
1635
                $ret .= ' alt="'.$title.'"';
1636
            } else {
1637
                $ret .= ' alt=""';
1638
            }
1639
1640
            if(!is_null($width))
1641
                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1642
1643
            if(!is_null($height))
1644
                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1645
1646
            $ret .= ' />';
1647
1648
        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1649
            // first get the $title
1650
            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1651
            if(!$render) {
1652
                // if the file is not supposed to be rendered
1653
                // return the title of the file (just the sourcename if there is no title)
1654
                return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src)));
1655
            }
1656
1657
            $att          = array();
1658
            $att['class'] = "media$align";
1659
            if($title) {
1660
                $att['title'] = $title;
1661
            }
1662
1663
            if(media_supportedav($mime, 'video')) {
1664
                //add video
1665
                $ret .= $this->_video($src, $width, $height, $att);
1666
            }
1667
            if(media_supportedav($mime, 'audio')) {
1668
                //add audio
1669
                $ret .= $this->_audio($src, $att);
1670
            }
1671
1672
        } elseif($mime == 'application/x-shockwave-flash') {
1673
            if(!$render) {
1674
                // if the flash is not supposed to be rendered
1675
                // return the title of the flash
1676
                if(!$title) {
1677
                    // just show the sourcename
1678
                    $title = utf8_basename(noNS($src));
1679
                }
1680
                return $this->_xmlEntities($title);
1681
            }
1682
1683
            $att          = array();
1684
            $att['class'] = "media$align";
1685
            if($align == 'right') $att['align'] = 'right';
1686
            if($align == 'left') $att['align'] = 'left';
1687
            $ret .= html_flashobject(
1688
                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1689
                array('quality' => 'high'),
1690
                null,
1691
                $att,
1692
                $this->_xmlEntities($title)
1693
            );
1694
        } elseif($title) {
1695
            // well at least we have a title to display
1696
            $ret .= $this->_xmlEntities($title);
1697
        } else {
1698
            // just show the sourcename
1699
            $ret .= $this->_xmlEntities(utf8_basename(noNS($src)));
1700
        }
1701
1702
        return $ret;
1703
    }
1704
1705
    /**
1706
     * Escape string for output
1707
     *
1708
     * @param $string
1709
     * @return string
1710
     */
1711
    function _xmlEntities($string) {
1712
        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1713
    }
1714
1715
    /**
1716
     * Creates a linkid from a headline
1717
     *
1718
     * @author Andreas Gohr <[email protected]>
1719
     * @param string  $title   The headline title
1720
     * @param boolean $create  Create a new unique ID?
1721
     * @return string
1722
     */
1723
    function _headerToLink($title, $create = false) {
1724
        if($create) {
1725
            return sectionID($title, $this->headers);
1726
        } else {
1727
            $check = false;
1728
            return sectionID($title, $check);
1729
        }
1730
    }
1731
1732
    /**
1733
     * Construct a title and handle images in titles
1734
     *
1735
     * @author Harry Fuecks <[email protected]>
1736
     * @param string|array $title    either string title or media array
1737
     * @param string       $default  default title if nothing else is found
1738
     * @param bool         $isImage  will be set to true if it's a media file
1739
     * @param null|string  $id       linked page id (used to extract title from first heading)
1740
     * @param string       $linktype content|navigation
1741
     * @return string      HTML of the title, might be full image tag or just escaped text
1742
     */
1743
    function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1744
        $isImage = false;
1745
        if(is_array($title)) {
1746
            $isImage = true;
1747
            return $this->_imageTitle($title);
1748
        } elseif(is_null($title) || trim($title) == '') {
1749
            if(useHeading($linktype) && $id) {
1750
                $heading = p_get_first_heading($id);
1751
                if(!blank($heading)) {
1752
                    return $this->_xmlEntities($heading);
1753
                }
1754
            }
1755
            return $this->_xmlEntities($default);
1756
        } else {
1757
            return $this->_xmlEntities($title);
1758
        }
1759
    }
1760
1761
    /**
1762
     * Returns HTML code for images used in link titles
1763
     *
1764
     * @author Andreas Gohr <[email protected]>
1765
     * @param array $img
1766
     * @return string HTML img tag or similar
1767
     */
1768
    function _imageTitle($img) {
1769
        global $ID;
1770
1771
        // some fixes on $img['src']
1772
        // see internalmedia() and externalmedia()
1773
        list($img['src']) = explode('#', $img['src'], 2);
1774
        if($img['type'] == 'internalmedia') {
1775
            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
1776
        }
1777
1778
        return $this->_media(
1779
            $img['src'],
1780
            $img['title'],
1781
            $img['align'],
1782
            $img['width'],
1783
            $img['height'],
1784
            $img['cache']
1785
        );
1786
    }
1787
1788
    /**
1789
     * helperfunction to return a basic link to a media
1790
     *
1791
     * used in internalmedia() and externalmedia()
1792
     *
1793
     * @author   Pierre Spring <[email protected]>
1794
     * @param string $src       media ID
1795
     * @param string $title     descriptive text
1796
     * @param string $align     left|center|right
1797
     * @param int    $width     width of media in pixel
1798
     * @param int    $height    height of media in pixel
1799
     * @param string $cache     cache|recache|nocache
1800
     * @param bool   $render    should the media be embedded inline or just linked
1801
     * @return array associative array with link config
1802
     */
1803
    function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1804
        global $conf;
1805
1806
        $link           = array();
1807
        $link['class']  = 'media';
1808
        $link['style']  = '';
1809
        $link['pre']    = '';
1810
        $link['suf']    = '';
1811
        $link['more']   = '';
1812
        $link['target'] = $conf['target']['media'];
1813
        if($conf['target']['media']) $link['rel'] = 'noopener';
1814
        $link['title']  = $this->_xmlEntities($src);
1815
        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1816
1817
        return $link;
1818
    }
1819
1820
    /**
1821
     * Embed video(s) in HTML
1822
     *
1823
     * @author Anika Henke <[email protected]>
1824
     * @author Schplurtz le Déboulonné <[email protected]>
1825
     *
1826
     * @param string $src         - ID of video to embed
1827
     * @param int    $width       - width of the video in pixels
1828
     * @param int    $height      - height of the video in pixels
1829
     * @param array  $atts        - additional attributes for the <video> tag
1830
     * @return string
1831
     */
1832
    function _video($src, $width, $height, $atts = null) {
1833
        // prepare width and height
1834
        if(is_null($atts)) $atts = array();
1835
        $atts['width']  = (int) $width;
1836
        $atts['height'] = (int) $height;
1837
        if(!$atts['width']) $atts['width'] = 320;
1838
        if(!$atts['height']) $atts['height'] = 240;
1839
1840
        $posterUrl = '';
1841
        $files = array();
1842
        $tracks = array();
1843
        $isExternal = media_isexternal($src);
1844
1845
        if ($isExternal) {
1846
            // take direct source for external files
1847
            list(/*ext*/, $srcMime) = mimetype($src);
1848
            $files[$srcMime] = $src;
1849
        } else {
1850
            // prepare alternative formats
1851
            $extensions   = array('webm', 'ogv', 'mp4');
1852
            $files        = media_alternativefiles($src, $extensions);
1853
            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1854
            $tracks       = media_trackfiles($src);
1855
            if(!empty($poster)) {
1856
                $posterUrl = ml(reset($poster), '', true, '&');
1857
            }
1858
        }
1859
1860
        $out = '';
1861
        // open video tag
1862
        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1863
        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1864
        $out .= '>'.NL;
1865
        $fallback = '';
1866
1867
        // output source for each alternative video format
1868
        foreach($files as $mime => $file) {
1869
            if ($isExternal) {
1870
                $url = $file;
1871
                $linkType = 'externalmedia';
1872
            } else {
1873
                $url = ml($file, '', true, '&');
1874
                $linkType = 'internalmedia';
1875
            }
1876
            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1877
1878
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1879
            // alternative content (just a link to the file)
1880
            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1881
        }
1882
1883
        // output each track if any
1884
        foreach( $tracks as $trackid => $info ) {
1885
            list( $kind, $srclang ) = array_map( 'hsc', $info );
1886
            $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
1887
            $out .= "label=\"$srclang\" ";
1888
            $out .= 'src="'.ml($trackid, '', true).'">'.NL;
1889
        }
1890
1891
        // finish
1892
        $out .= $fallback;
1893
        $out .= '</video>'.NL;
1894
        return $out;
1895
    }
1896
1897
    /**
1898
     * Embed audio in HTML
1899
     *
1900
     * @author Anika Henke <[email protected]>
1901
     *
1902
     * @param string $src       - ID of audio to embed
1903
     * @param array  $atts      - additional attributes for the <audio> tag
1904
     * @return string
1905
     */
1906
    function _audio($src, $atts = array()) {
1907
        $files = array();
1908
        $isExternal = media_isexternal($src);
1909
1910
        if ($isExternal) {
1911
            // take direct source for external files
1912
            list(/*ext*/, $srcMime) = mimetype($src);
1913
            $files[$srcMime] = $src;
1914
        } else {
1915
            // prepare alternative formats
1916
            $extensions   = array('ogg', 'mp3', 'wav');
1917
            $files        = media_alternativefiles($src, $extensions);
1918
        }
1919
1920
        $out = '';
1921
        // open audio tag
1922
        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1923
        $fallback = '';
1924
1925
        // output source for each alternative audio format
1926
        foreach($files as $mime => $file) {
1927
            if ($isExternal) {
1928
                $url = $file;
1929
                $linkType = 'externalmedia';
1930
            } else {
1931
                $url = ml($file, '', true, '&');
1932
                $linkType = 'internalmedia';
1933
            }
1934
            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file)));
1935
1936
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1937
            // alternative content (just a link to the file)
1938
            $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true);
1939
        }
1940
1941
        // finish
1942
        $out .= $fallback;
1943
        $out .= '</audio>'.NL;
1944
        return $out;
1945
    }
1946
1947
    /**
1948
     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1949
     * which returns an existing media revision less or equal to rev or date_at
1950
     *
1951
     * @author lisps
1952
     * @param string $media_id
1953
     * @access protected
1954
     * @return string revision ('' for current)
1955
     */
1956
    function _getLastMediaRevisionAt($media_id){
1957
        if(!$this->date_at || media_isexternal($media_id)) return '';
1958
        $pagelog = new MediaChangeLog($media_id);
1959
        return $pagelog->getLastRevisionAt($this->date_at);
1960
    }
1961
1962
    #endregion
1963
}
1964
1965
//Setup VIM: ex: et ts=4 :
1966