Failed Conditions
Push — psr2 ( 9ddafc...b78f68 )
by Andreas
11:21 queued 07:11
created

inc/parser/xhtml.php (1 issue)

Labels
Severity

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
use dokuwiki\ChangeLog\MediaChangeLog;
0 ignored issues
show
This use statement conflicts with another class in this namespace, MediaChangeLog.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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