Failed Conditions
Push — interwiki-remove-golucky ( 52fcdb...768be5 )
by Henry
15:30 queued 12:50
created

Doku_Renderer_xhtml::getLastlevel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
use dokuwiki\ChangeLog\MediaChangeLog;
0 ignored issues
show
Bug introduced by
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) {
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...
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) {
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...
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);
0 ignored issues
show
Security Bug introduced by
It seems like getNS($ID) targeting getNS() can also be of type false; however, resolve_pageid() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $search 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...
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);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type null; however, Doku_Renderer_xhtml::_getLinkTitle() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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'] .= ' ugc 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
            if($url == '') return $link['name'];
1068
            return $this->_formatLink($link);
1069
        } else {
1070
            if($url == '') $this->doc .= $link['name'];
1071
            else $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
    public 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);
0 ignored issues
show
Bug introduced by
It seems like $name defined by parameter $name on line 1083 can also be of type null; however, Doku_Renderer_xhtml::_getLinkTitle() does only seem to accept string|array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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
    public 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);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type null; however, Doku_Renderer_xhtml::_getLinkTitle() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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
    public 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);
0 ignored issues
show
Security Bug introduced by
It seems like getNS($ID) targeting getNS() can also be of type false; however, resolve_mediaid() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
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(
1190
                $src,
1191
                array(
1192
                    'id' => $ID,
1193
                    'cache' => $cache,
1194
                    'rev' => $this->_getLastMediaRevisionAt($src)
1195
                ),
1196
                ($linking == 'direct')
1197
            );
1198
        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1199
            // don't link movies
1200
            $noLink = true;
1201
        } else {
1202
            // add file icons
1203
            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1204
            $link['class'] .= ' mediafile mf_'.$class;
1205
            $link['url'] = ml(
1206
                $src,
1207
                array(
1208
                    'id' => $ID,
1209
                    'cache' => $cache,
1210
                    'rev' => $this->_getLastMediaRevisionAt($src)
1211
                ),
1212
                true
1213
            );
1214
            if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')';
1215
        }
1216
1217
        if (!empty($hash)) $link['url'] .= '#'.$hash;
1218
1219
        //markup non existing files
1220
        if(!$exists) {
1221
            $link['class'] .= ' wikilink2';
1222
        }
1223
1224
        //output formatted
1225
        if($return) {
1226
            if($linking == 'nolink' || $noLink) return $link['name'];
1227
            else return $this->_formatLink($link);
1228
        } else {
1229
            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1230
            else $this->doc .= $this->_formatLink($link);
1231
        }
1232
    }
1233
1234
    /**
1235
     * Render an external media file
1236
     *
1237
     * @param string $src     full media URL
1238
     * @param string $title   descriptive text
1239
     * @param string $align   left|center|right
1240
     * @param int    $width   width of media in pixel
1241
     * @param int    $height  height of media in pixel
1242
     * @param string $cache   cache|recache|nocache
1243
     * @param string $linking linkonly|detail|nolink
1244
     * @param bool   $return  return HTML instead of adding to $doc
1245
     * @return void|string writes to doc attribute or returns html depends on $return
1246
     */
1247
    public function externalmedia($src, $title = null, $align = null, $width = null,
1248
                           $height = null, $cache = null, $linking = null, $return = false) {
1249
        if(link_isinterwiki($src)){
1250
            list($shortcut, $reference) = explode('>', $src, 2);
1251
            $exists = null;
1252
            $src = $this->_resolveInterWiki($shortcut, $reference, $exists);
1253
            if($src == '' && empty($title)){
1254
                // make sure at least something will be shown in this case
1255
                $title = $reference;
1256
            }
1257
        }
1258
        list($src, $hash) = explode('#', $src, 2);
1259
        $noLink = false;
1260
        if($src == '') {
1261
            // only output plaintext without link if there is no src
1262
            $noLink = true;
1263
        }
1264
        $render = ($linking == 'linkonly') ? false : true;
1265
        $link   = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render);
1266
1267
        $link['url'] = ml($src, array('cache' => $cache));
1268
1269
        list($ext, $mime) = mimetype($src, false);
1270
        if(substr($mime, 0, 5) == 'image' && $render) {
1271
            // link only jpeg images
1272
            // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true;
1273
        } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) {
1274
            // don't link movies
1275
            $noLink = true;
1276
        } else {
1277
            // add file icons
1278
            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
1279
            $link['class'] .= ' mediafile mf_'.$class;
1280
        }
1281
1282
        if($hash) $link['url'] .= '#'.$hash;
1283
1284
        //output formatted
1285
        if($return) {
1286
            if($linking == 'nolink' || $noLink) return $link['name'];
1287
            else return $this->_formatLink($link);
1288
        } else {
1289
            if($linking == 'nolink' || $noLink) $this->doc .= $link['name'];
1290
            else $this->doc .= $this->_formatLink($link);
1291
        }
1292
    }
1293
1294
    /**
1295
     * Renders an RSS feed
1296
     *
1297
     * @param string $url    URL of the feed
1298
     * @param array  $params Finetuning of the output
1299
     *
1300
     * @author Andreas Gohr <[email protected]>
1301
     */
1302
    public function rss($url, $params) {
1303
        global $lang;
1304
        global $conf;
1305
1306
        require_once(DOKU_INC.'inc/FeedParser.php');
1307
        $feed = new FeedParser();
1308
        $feed->set_feed_url($url);
1309
1310
        //disable warning while fetching
1311
        if(!defined('DOKU_E_LEVEL')) {
1312
            $elvl = error_reporting(E_ERROR);
1313
        }
1314
        $rc = $feed->init();
1315
        if(isset($elvl)) {
1316
            error_reporting($elvl);
1317
        }
1318
1319
        if($params['nosort']) $feed->enable_order_by_date(false);
1320
1321
        //decide on start and end
1322
        if($params['reverse']) {
1323
            $mod   = -1;
1324
            $start = $feed->get_item_quantity() - 1;
1325
            $end   = $start - ($params['max']);
1326
            $end   = ($end < -1) ? -1 : $end;
1327
        } else {
1328
            $mod   = 1;
1329
            $start = 0;
1330
            $end   = $feed->get_item_quantity();
1331
            $end   = ($end > $params['max']) ? $params['max'] : $end;
1332
        }
1333
1334
        $this->doc .= '<ul class="rss">';
1335
        if($rc) {
1336
            for($x = $start; $x != $end; $x += $mod) {
1337
                $item = $feed->get_item($x);
1338
                $this->doc .= '<li><div class="li">';
1339
                // support feeds without links
1340
                $lnkurl = $item->get_permalink();
1341
                if($lnkurl) {
1342
                    // title is escaped by SimplePie, we unescape here because it
1343
                    // is escaped again in externallink() FS#1705
1344
                    $this->externallink(
1345
                        $item->get_permalink(),
1346
                        html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')
1347
                    );
1348
                } else {
1349
                    $this->doc .= ' '.$item->get_title();
1350
                }
1351
                if($params['author']) {
1352
                    $author = $item->get_author(0);
1353
                    if($author) {
1354
                        $name = $author->get_name();
1355
                        if(!$name) $name = $author->get_email();
1356
                        if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name);
1357
                    }
1358
                }
1359
                if($params['date']) {
1360
                    $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')';
1361
                }
1362
                if($params['details']) {
1363
                    $this->doc .= '<div class="detail">';
1364
                    if($conf['htmlok']) {
1365
                        $this->doc .= $item->get_description();
1366
                    } else {
1367
                        $this->doc .= strip_tags($item->get_description());
1368
                    }
1369
                    $this->doc .= '</div>';
1370
                }
1371
1372
                $this->doc .= '</div></li>';
1373
            }
1374
        } else {
1375
            $this->doc .= '<li><div class="li">';
1376
            $this->doc .= '<em>'.$lang['rssfailed'].'</em>';
1377
            $this->externallink($url);
1378
            if($conf['allowdebug']) {
1379
                $this->doc .= '<!--'.hsc($feed->error).'-->';
1380
            }
1381
            $this->doc .= '</div></li>';
1382
        }
1383
        $this->doc .= '</ul>';
1384
    }
1385
1386
    /**
1387
     * Start a table
1388
     *
1389
     * @param int $maxcols maximum number of columns
1390
     * @param int $numrows NOT IMPLEMENTED
1391
     * @param int $pos byte position in the original source
1392
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1393
     */
1394
    public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) {
1395
        // initialize the row counter used for classes
1396
        $this->_counter['row_counter'] = 0;
1397
        $class                         = 'table';
1398
        if($classes !== null) {
1399
            if(is_array($classes)) $classes = join(' ', $classes);
1400
            $class .= ' ' . $classes;
1401
        }
1402
        if($pos !== null) {
1403
            $hid = $this->_headerToLink($class, true);
1404
            $data = array();
1405
            $data['target'] = 'table';
1406
            $data['name'] = '';
1407
            $data['hid'] = $hid;
1408
            $class .= ' '.$this->startSectionEdit($pos, $data);
1409
        }
1410
        $this->doc .= '<div class="'.$class.'"><table class="inline">'.
1411
            DOKU_LF;
1412
    }
1413
1414
    /**
1415
     * Close a table
1416
     *
1417
     * @param int $pos byte position in the original source
1418
     */
1419
    public function table_close($pos = null) {
1420
        $this->doc .= '</table></div>'.DOKU_LF;
1421
        if($pos !== null) {
1422
            $this->finishSectionEdit($pos);
1423
        }
1424
    }
1425
1426
    /**
1427
     * Open a table header
1428
     */
1429
    public function tablethead_open() {
1430
        $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF;
1431
    }
1432
1433
    /**
1434
     * Close a table header
1435
     */
1436
    public function tablethead_close() {
1437
        $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF;
1438
    }
1439
1440
    /**
1441
     * Open a table body
1442
     */
1443
    public function tabletbody_open() {
1444
        $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF;
1445
    }
1446
1447
    /**
1448
     * Close a table body
1449
     */
1450
    public function tabletbody_close() {
1451
        $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF;
1452
    }
1453
1454
    /**
1455
     * Open a table footer
1456
     */
1457
    public function tabletfoot_open() {
1458
        $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF;
1459
    }
1460
1461
    /**
1462
     * Close a table footer
1463
     */
1464
    public function tabletfoot_close() {
1465
        $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF;
1466
    }
1467
1468
    /**
1469
     * Open a table row
1470
     *
1471
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1472
     */
1473
    public function tablerow_open($classes = null) {
1474
        // initialize the cell counter used for classes
1475
        $this->_counter['cell_counter'] = 0;
1476
        $class                          = 'row'.$this->_counter['row_counter']++;
1477
        if($classes !== null) {
1478
            if(is_array($classes)) $classes = join(' ', $classes);
1479
            $class .= ' ' . $classes;
1480
        }
1481
        $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
1482
    }
1483
1484
    /**
1485
     * Close a table row
1486
     */
1487
    public function tablerow_close() {
1488
        $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF;
1489
    }
1490
1491
    /**
1492
     * Open a table header cell
1493
     *
1494
     * @param int    $colspan
1495
     * @param string $align left|center|right
1496
     * @param int    $rowspan
1497
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
1498
     */
1499
    public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1500
        $class = 'class="col'.$this->_counter['cell_counter']++;
1501
        if(!is_null($align)) {
1502
            $class .= ' '.$align.'align';
1503
        }
1504
        if($classes !== null) {
1505
            if(is_array($classes)) $classes = join(' ', $classes);
1506
            $class .= ' ' . $classes;
1507
        }
1508
        $class .= '"';
1509
        $this->doc .= '<th '.$class;
1510
        if($colspan > 1) {
1511
            $this->_counter['cell_counter'] += $colspan - 1;
1512
            $this->doc .= ' colspan="'.$colspan.'"';
1513
        }
1514
        if($rowspan > 1) {
1515
            $this->doc .= ' rowspan="'.$rowspan.'"';
1516
        }
1517
        $this->doc .= '>';
1518
    }
1519
1520
    /**
1521
     * Close a table header cell
1522
     */
1523
    public function tableheader_close() {
1524
        $this->doc .= '</th>';
1525
    }
1526
1527
    /**
1528
     * Open a table cell
1529
     *
1530
     * @param int       $colspan
1531
     * @param string    $align left|center|right
1532
     * @param int       $rowspan
1533
     * @param string|string[]    $classes css classes - have to be valid, do not pass unfiltered user input
1534
     */
1535
    public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) {
1536
        $class = 'class="col'.$this->_counter['cell_counter']++;
1537
        if(!is_null($align)) {
1538
            $class .= ' '.$align.'align';
1539
        }
1540
        if($classes !== null) {
1541
            if(is_array($classes)) $classes = join(' ', $classes);
1542
            $class .= ' ' . $classes;
1543
        }
1544
        $class .= '"';
1545
        $this->doc .= '<td '.$class;
1546
        if($colspan > 1) {
1547
            $this->_counter['cell_counter'] += $colspan - 1;
1548
            $this->doc .= ' colspan="'.$colspan.'"';
1549
        }
1550
        if($rowspan > 1) {
1551
            $this->doc .= ' rowspan="'.$rowspan.'"';
1552
        }
1553
        $this->doc .= '>';
1554
    }
1555
1556
    /**
1557
     * Close a table cell
1558
     */
1559
    public function tablecell_close() {
1560
        $this->doc .= '</td>';
1561
    }
1562
1563
    /**
1564
     * Returns the current header level.
1565
     * (required e.g. by the filelist plugin)
1566
     *
1567
     * @return int The current header level
1568
     */
1569
    public function getLastlevel() {
1570
        return $this->lastlevel;
1571
    }
1572
1573
    #region Utility functions
1574
1575
    /**
1576
     * Build a link
1577
     *
1578
     * Assembles all parts defined in $link returns HTML for the link
1579
     *
1580
     * @param array $link attributes of a link
1581
     * @return string
1582
     *
1583
     * @author Andreas Gohr <[email protected]>
1584
     */
1585
    public function _formatLink($link) {
1586
        //make sure the url is XHTML compliant (skip mailto)
1587
        if(substr($link['url'], 0, 7) != 'mailto:') {
1588
            $link['url'] = str_replace('&', '&amp;', $link['url']);
1589
            $link['url'] = str_replace('&amp;amp;', '&amp;', $link['url']);
1590
        }
1591
        //remove double encodings in titles
1592
        $link['title'] = str_replace('&amp;amp;', '&amp;', $link['title']);
1593
1594
        // be sure there are no bad chars in url or title
1595
        // (we can't do this for name because it can contain an img tag)
1596
        $link['url']   = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22'));
1597
        $link['title'] = strtr($link['title'], array('>' => '&gt;', '<' => '&lt;', '"' => '&quot;'));
1598
1599
        $ret = '';
1600
        $ret .= $link['pre'];
1601
        $ret .= '<a href="'.$link['url'].'"';
1602
        if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"';
1603
        if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"';
1604
        if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"';
1605
        if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"';
1606
        if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"';
1607
        if(!empty($link['more'])) $ret .= ' '.$link['more'];
1608
        $ret .= '>';
1609
        $ret .= $link['name'];
1610
        $ret .= '</a>';
1611
        $ret .= $link['suf'];
1612
        return $ret;
1613
    }
1614
1615
    /**
1616
     * Renders internal and external media
1617
     *
1618
     * @author Andreas Gohr <[email protected]>
1619
     * @param string $src       media ID
1620
     * @param string $title     descriptive text
1621
     * @param string $align     left|center|right
1622
     * @param int    $width     width of media in pixel
1623
     * @param int    $height    height of media in pixel
1624
     * @param string $cache     cache|recache|nocache
1625
     * @param bool   $render    should the media be embedded inline or just linked
1626
     * @return string
1627
     */
1628
    public function _media($src, $title = null, $align = null, $width = null,
1629
                    $height = null, $cache = null, $render = true) {
1630
1631
        $ret = '';
1632
1633
        list($ext, $mime) = mimetype($src);
1634
        if(substr($mime, 0, 5) == 'image') {
1635
            // first get the $title
1636
            if(!is_null($title)) {
1637
                $title = $this->_xmlEntities($title);
1638
            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1639
                //try to use the caption from IPTC/EXIF
1640
                require_once(DOKU_INC.'inc/JpegMeta.php');
1641
                $jpeg = new JpegMeta(mediaFN($src));
1642
                if($jpeg !== false) $cap = $jpeg->getTitle();
1643
                if(!empty($cap)) {
1644
                    $title = $this->_xmlEntities($cap);
1645
                }
1646
            }
1647
            if(!$render) {
1648
                // if the picture is not supposed to be rendered
1649
                // return the title of the picture
1650
                if($title === null || $title === "") {
1651
                    // just show the sourcename
1652
                    $title = $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1653
                }
1654
                return $title;
1655
            }
1656
            //add image tag
1657
            $ret .= '<img src="' . ml(
1658
                    $src,
1659
                    array(
1660
                        'w' => $width, 'h' => $height,
1661
                        'cache' => $cache,
1662
                        'rev' => $this->_getLastMediaRevisionAt($src)
1663
                    )
1664
                ) . '"';
1665
            $ret .= ' class="media'.$align.'"';
1666
1667
            if($title) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title 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...
1668
                $ret .= ' title="'.$title.'"';
1669
                $ret .= ' alt="'.$title.'"';
1670
            } else {
1671
                $ret .= ' alt=""';
1672
            }
1673
1674
            if(!is_null($width))
1675
                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1676
1677
            if(!is_null($height))
1678
                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1679
1680
            $ret .= ' />';
1681
1682
        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1683
            // first get the $title
1684
            $title = !is_null($title) ? $this->_xmlEntities($title) : false;
1685
            if(!$render) {
1686
                // if the file is not supposed to be rendered
1687
                // return the title of the file (just the sourcename if there is no title)
1688
                return $title ? $title : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1689
            }
1690
1691
            $att          = array();
1692
            $att['class'] = "media$align";
1693
            if($title) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
1694
                $att['title'] = $title;
1695
            }
1696
1697
            if(media_supportedav($mime, 'video')) {
1698
                //add video
1699
                $ret .= $this->_video($src, $width, $height, $att);
1700
            }
1701
            if(media_supportedav($mime, 'audio')) {
1702
                //add audio
1703
                $ret .= $this->_audio($src, $att);
1704
            }
1705
1706
        } elseif($mime == 'application/x-shockwave-flash') {
1707
            if(!$render) {
1708
                // if the flash is not supposed to be rendered
1709
                // return the title of the flash
1710
                if(!$title) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|null is loosely compared to false; 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...
1711
                    // just show the sourcename
1712
                    $title = \dokuwiki\Utf8\PhpString::basename(noNS($src));
1713
                }
1714
                return $this->_xmlEntities($title);
1715
            }
1716
1717
            $att          = array();
1718
            $att['class'] = "media$align";
1719
            if($align == 'right') $att['align'] = 'right';
1720
            if($align == 'left') $att['align'] = 'left';
1721
            $ret .= html_flashobject(
1722
                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1723
                array('quality' => 'high'),
1724
                null,
1725
                $att,
1726
                $this->_xmlEntities($title)
1727
            );
1728
        } elseif($title) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title 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...
1729
            // well at least we have a title to display
1730
            $ret .= $this->_xmlEntities($title);
1731
        } else {
1732
            // just show the sourcename
1733
            $ret .= $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1734
        }
1735
1736
        return $ret;
1737
    }
1738
1739
    /**
1740
     * Escape string for output
1741
     *
1742
     * @param $string
1743
     * @return string
1744
     */
1745
    public function _xmlEntities($string) {
1746
        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1747
    }
1748
1749
1750
1751
    /**
1752
     * Construct a title and handle images in titles
1753
     *
1754
     * @author Harry Fuecks <[email protected]>
1755
     * @param string|array $title    either string title or media array
1756
     * @param string       $default  default title if nothing else is found
1757
     * @param bool         $isImage  will be set to true if it's a media file
1758
     * @param null|string  $id       linked page id (used to extract title from first heading)
1759
     * @param string       $linktype content|navigation
1760
     * @return string      HTML of the title, might be full image tag or just escaped text
1761
     */
1762
    public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1763
        $isImage = false;
1764
        if(is_array($title)) {
1765
            $isImage = true;
1766
            return $this->_imageTitle($title);
1767
        } elseif(is_null($title) || trim($title) == '') {
1768
            if(useHeading($linktype) && $id) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type null|string 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...
1769
                $heading = p_get_first_heading($id);
1770
                if(!blank($heading)) {
1771
                    return $this->_xmlEntities($heading);
1772
                }
1773
            }
1774
            return $this->_xmlEntities($default);
1775
        } else {
1776
            return $this->_xmlEntities($title);
1777
        }
1778
    }
1779
1780
    /**
1781
     * Returns HTML code for images used in link titles
1782
     *
1783
     * @author Andreas Gohr <[email protected]>
1784
     * @param array $img
1785
     * @return string HTML img tag or similar
1786
     */
1787
    public function _imageTitle($img) {
1788
        global $ID;
1789
1790
        // some fixes on $img['src']
1791
        // see internalmedia() and externalmedia()
1792
        list($img['src']) = explode('#', $img['src'], 2);
1793
        if($img['type'] == 'internalmedia') {
1794
            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
0 ignored issues
show
Security Bug introduced by
It seems like getNS($ID) targeting getNS() can also be of type false; however, resolve_mediaid() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1795
        }
1796
1797
        return $this->_media(
1798
            $img['src'],
1799
            $img['title'],
1800
            $img['align'],
1801
            $img['width'],
1802
            $img['height'],
1803
            $img['cache']
1804
        );
1805
    }
1806
1807
    /**
1808
     * helperfunction to return a basic link to a media
1809
     *
1810
     * used in internalmedia() and externalmedia()
1811
     *
1812
     * @author   Pierre Spring <[email protected]>
1813
     * @param string $src       media ID
1814
     * @param string $title     descriptive text
1815
     * @param string $align     left|center|right
1816
     * @param int    $width     width of media in pixel
1817
     * @param int    $height    height of media in pixel
1818
     * @param string $cache     cache|recache|nocache
1819
     * @param bool   $render    should the media be embedded inline or just linked
1820
     * @return array associative array with link config
1821
     */
1822
    public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1823
        global $conf;
1824
1825
        $link           = array();
1826
        $link['class']  = 'media';
1827
        $link['style']  = '';
1828
        $link['pre']    = '';
1829
        $link['suf']    = '';
1830
        $link['more']   = '';
1831
        $link['target'] = $conf['target']['media'];
1832
        if($conf['target']['media']) $link['rel'] = 'noopener';
1833
        $link['title']  = $this->_xmlEntities($src);
1834
        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1835
1836
        return $link;
1837
    }
1838
1839
    /**
1840
     * Embed video(s) in HTML
1841
     *
1842
     * @author Anika Henke <[email protected]>
1843
     * @author Schplurtz le Déboulonné <[email protected]>
1844
     *
1845
     * @param string $src         - ID of video to embed
1846
     * @param int    $width       - width of the video in pixels
1847
     * @param int    $height      - height of the video in pixels
1848
     * @param array  $atts        - additional attributes for the <video> tag
1849
     * @return string
1850
     */
1851
    public function _video($src, $width, $height, $atts = null) {
1852
        // prepare width and height
1853
        if(is_null($atts)) $atts = array();
1854
        $atts['width']  = (int) $width;
1855
        $atts['height'] = (int) $height;
1856
        if(!$atts['width']) $atts['width'] = 320;
1857
        if(!$atts['height']) $atts['height'] = 240;
1858
1859
        $posterUrl = '';
1860
        $files = array();
1861
        $tracks = array();
1862
        $isExternal = media_isexternal($src);
1863
1864
        if ($isExternal) {
1865
            // take direct source for external files
1866
            list(/*ext*/, $srcMime) = mimetype($src);
1867
            $files[$srcMime] = $src;
1868
        } else {
1869
            // prepare alternative formats
1870
            $extensions   = array('webm', 'ogv', 'mp4');
1871
            $files        = media_alternativefiles($src, $extensions);
1872
            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1873
            $tracks       = media_trackfiles($src);
1874
            if(!empty($poster)) {
1875
                $posterUrl = ml(reset($poster), '', true, '&');
1876
            }
1877
        }
1878
1879
        $out = '';
1880
        // open video tag
1881
        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1882
        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1883
        $out .= '>'.NL;
1884
        $fallback = '';
1885
1886
        // output source for each alternative video format
1887
        foreach($files as $mime => $file) {
1888
            if ($isExternal) {
1889
                $url = $file;
1890
                $linkType = 'externalmedia';
1891
            } else {
1892
                $url = ml($file, '', true, '&');
1893
                $linkType = 'internalmedia';
1894
            }
1895
            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1896
1897
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1898
            // alternative content (just a link to the file)
1899
            $fallback .= $this->$linkType(
1900
                $file,
1901
                $title,
1902
                null,
1903
                null,
1904
                null,
1905
                $cache = null,
1906
                $linking = 'linkonly',
1907
                $return = true
1908
            );
1909
        }
1910
1911
        // output each track if any
1912
        foreach( $tracks as $trackid => $info ) {
1913
            list( $kind, $srclang ) = array_map( 'hsc', $info );
1914
            $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
1915
            $out .= "label=\"$srclang\" ";
1916
            $out .= 'src="'.ml($trackid, '', true).'">'.NL;
1917
        }
1918
1919
        // finish
1920
        $out .= $fallback;
1921
        $out .= '</video>'.NL;
1922
        return $out;
1923
    }
1924
1925
    /**
1926
     * Embed audio in HTML
1927
     *
1928
     * @author Anika Henke <[email protected]>
1929
     *
1930
     * @param string $src       - ID of audio to embed
1931
     * @param array  $atts      - additional attributes for the <audio> tag
1932
     * @return string
1933
     */
1934
    public function _audio($src, $atts = array()) {
1935
        $files = array();
1936
        $isExternal = media_isexternal($src);
1937
1938
        if ($isExternal) {
1939
            // take direct source for external files
1940
            list(/*ext*/, $srcMime) = mimetype($src);
1941
            $files[$srcMime] = $src;
1942
        } else {
1943
            // prepare alternative formats
1944
            $extensions   = array('ogg', 'mp3', 'wav');
1945
            $files        = media_alternativefiles($src, $extensions);
1946
        }
1947
1948
        $out = '';
1949
        // open audio tag
1950
        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1951
        $fallback = '';
1952
1953
        // output source for each alternative audio format
1954
        foreach($files as $mime => $file) {
1955
            if ($isExternal) {
1956
                $url = $file;
1957
                $linkType = 'externalmedia';
1958
            } else {
1959
                $url = ml($file, '', true, '&');
1960
                $linkType = 'internalmedia';
1961
            }
1962
            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1963
1964
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1965
            // alternative content (just a link to the file)
1966
            $fallback .= $this->$linkType(
1967
                $file,
1968
                $title,
1969
                null,
1970
                null,
1971
                null,
1972
                $cache = null,
1973
                $linking = 'linkonly',
1974
                $return = true
1975
            );
1976
        }
1977
1978
        // finish
1979
        $out .= $fallback;
1980
        $out .= '</audio>'.NL;
1981
        return $out;
1982
    }
1983
1984
    /**
1985
     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1986
     * which returns an existing media revision less or equal to rev or date_at
1987
     *
1988
     * @author lisps
1989
     * @param string $media_id
1990
     * @access protected
1991
     * @return string revision ('' for current)
1992
     */
1993
    protected function _getLastMediaRevisionAt($media_id){
1994
        if(!$this->date_at || media_isexternal($media_id)) return '';
1995
        $pagelog = new MediaChangeLog($media_id);
1996
        return $pagelog->getLastRevisionAt($this->date_at);
1997
    }
1998
1999
    #endregion
2000
}
2001
2002
//Setup VIM: ex: et ts=4 :
2003