Failed Conditions
Push — master ( 8a0050...038768 )
by Andreas
08:58 queued 05:45
created

inc/parser/xhtml.php (4 issues)

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'.hsc(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) {
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
     * @param array  $options  assoziative array with additional geshi options
637
     */
638
    function file($text, $language = null, $filename = null, $options=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...
639
        $this->_highlight('file', $text, $language, $filename, $options);
640
    }
641
642
    /**
643
     * Display text as code content, optionally syntax highlighted
644
     *
645
     * @param string $text     text to show
646
     * @param string $language programming language to use for syntax highlighting
647
     * @param string $filename file path label
648
     * @param array  $options  assoziative array with additional geshi options
649
     */
650
    function code($text, $language = null, $filename = null, $options=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...
651
        $this->_highlight('code', $text, $language, $filename, $options);
652
    }
653
654
    /**
655
     * Use GeSHi to highlight language syntax in code and file blocks
656
     *
657
     * @author Andreas Gohr <[email protected]>
658
     * @param string $type     code|file
659
     * @param string $text     text to show
660
     * @param string $language programming language to use for syntax highlighting
661
     * @param string $filename file path label
662
     * @param array  $options  assoziative array with additional geshi options
663
     */
664
    function _highlight($type, $text, $language = null, $filename = null, $options = 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...
665
        global $ID;
666
        global $lang;
667
        global $INPUT;
668
669
        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
670
671
        if($filename) {
672
            // add icon
673
            list($ext) = mimetype($filename, false);
674
            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
675
            $class = 'mediafile mf_'.$class;
676
677
            $offset = 0;
678
            if ($INPUT->has('codeblockOffset')) {
679
                $offset = $INPUT->str('codeblockOffset');
680
            }
681
            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
682
            $this->doc .= '<dt><a href="'.exportlink($ID, 'code', array('codeblock' => $offset+$this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">';
683
            $this->doc .= hsc($filename);
684
            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
685
        }
686
687
        if($text{0} == "\n") {
688
            $text = substr($text, 1);
689
        }
690
        if(substr($text, -1) == "\n") {
691
            $text = substr($text, 0, -1);
692
        }
693
694
        if(empty($language)) { // empty is faster than is_null and can prevent '' string
695
            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
696
        } else {
697
            $class = 'code'; //we always need the code class to make the syntax highlighting apply
698
            if($type != 'code') $class .= ' '.$type;
699
700
            $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '', $options).'</pre>'.DOKU_LF;
701
        }
702
703
        if($filename) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

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