Completed
Pull Request — master (#3347)
by
unknown
02:59
created

Doku_Renderer_xhtml::toc_additem()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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