Issues (847)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

inc/parser/xhtml.php (15 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
use dokuwiki\ChangeLog\MediaChangeLog;
0 ignored issues
show
This use statement conflicts with another class in this namespace, MediaChangeLog.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
4
5
/**
6
 * Renderer for XHTML output
7
 *
8
 * This is DokuWiki's main renderer used to display page content in the wiki
9
 *
10
 * @author Harry Fuecks <[email protected]>
11
 * @author Andreas Gohr <[email protected]>
12
 *
13
 */
14
class Doku_Renderer_xhtml extends Doku_Renderer {
15
    /** @var array store the table of contents */
16
    public $toc = array();
17
18
    /** @var array A stack of section edit data */
19
    protected $sectionedits = array();
20
21
    /** @var string|int link pages and media against this revision */
22
    public $date_at = '';
23
24
    /** @var int last section edit id, used by startSectionEdit */
25
    protected $lastsecid = 0;
26
27
    /** @var array a list of footnotes, list starts at 1! */
28
    protected $footnotes = array();
29
30
    /** @var int current section level */
31
    protected $lastlevel = 0;
32
    /** @var array section node tracker */
33
    protected $node = array(0, 0, 0, 0, 0);
34
35
    /** @var string temporary $doc store */
36
    protected $store = '';
37
38
    /** @var array global counter, for table classes etc. */
39
    protected $_counter = array(); //
40
41
    /** @var int counts the code and file blocks, used to provide download links */
42
    protected $_codeblock = 0;
43
44
    /** @var array list of allowed URL schemes */
45
    protected $schemes = null;
46
47
    /**
48
     * Register a new edit section range
49
     *
50
     * @param int    $start  The byte position for the edit start
51
     * @param array  $data   Associative array with section data:
52
     *                       Key 'name': the section name/title
53
     *                       Key 'target': the target for the section edit,
54
     *                                     e.g. 'section' or 'table'
55
     *                       Key 'hid': header id
56
     *                       Key 'codeblockOffset': actual code block index
57
     *                       Key 'start': set in startSectionEdit(),
58
     *                                    do not set yourself
59
     *                       Key 'range': calculated from 'start' and
60
     *                                    $key in finishSectionEdit(),
61
     *                                    do not set yourself
62
     * @return string  A marker class for the starting HTML element
63
     *
64
     * @author Adrian Lang <[email protected]>
65
     */
66
    public function startSectionEdit($start, $data) {
67
        if (!is_array($data)) {
68
            msg(
69
                sprintf(
70
                    'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
71
                    hsc((string) $data)
72
                ), -1
73
            );
74
75
            // @deprecated 2018-04-14, backward compatibility
76
            $args = func_get_args();
77
            $data = array();
78
            if(isset($args[1])) $data['target'] = $args[1];
79
            if(isset($args[2])) $data['name'] = $args[2];
80
            if(isset($args[3])) $data['hid'] = $args[3];
81
        }
82
        $data['secid'] = ++$this->lastsecid;
83
        $data['start'] = $start;
84
        $this->sectionedits[] = $data;
85
        return 'sectionedit'.$data['secid'];
86
    }
87
88
    /**
89
     * Finish an edit section range
90
     *
91
     * @param int  $end     The byte position for the edit end; null for the rest of the page
92
     *
93
     * @author Adrian Lang <[email protected]>
94
     */
95
    public function finishSectionEdit($end = null, $hid = null) {
96
        $data = array_pop($this->sectionedits);
97
        if(!is_null($end) && $end <= $data['start']) {
98
            return;
99
        }
100
        if(!is_null($hid)) {
101
            $data['hid'] .= $hid;
102
        }
103
        $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end);
104
        unset($data['start']);
105
        $this->doc .= '<!-- EDIT'.hsc(json_encode ($data)).' -->';
106
    }
107
108
    /**
109
     * Returns the format produced by this renderer.
110
     *
111
     * @return string always 'xhtml'
112
     */
113
    public function getFormat() {
114
        return 'xhtml';
115
    }
116
117
    /**
118
     * Initialize the document
119
     */
120
    public function document_start() {
121
        //reset some internals
122
        $this->toc     = array();
123
    }
124
125
    /**
126
     * Finalize the document
127
     */
128
    public function document_end() {
129
        // Finish open section edits.
130
        while(count($this->sectionedits) > 0) {
131
            if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
132
                // If there is only one section, do not write a section edit
133
                // marker.
134
                array_pop($this->sectionedits);
135
            } else {
136
                $this->finishSectionEdit();
137
            }
138
        }
139
140
        if(count($this->footnotes) > 0) {
141
            $this->doc .= '<div class="footnotes">'.DOKU_LF;
142
143
            foreach($this->footnotes as $id => $footnote) {
144
                // check its not a placeholder that indicates actual footnote text is elsewhere
145
                if(substr($footnote, 0, 5) != "@@FNT") {
146
147
                    // open the footnote and set the anchor and backlink
148
                    $this->doc .= '<div class="fn">';
149
                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
150
                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
151
152
                    // get any other footnotes that use the same markup
153
                    $alt = array_keys($this->footnotes, "@@FNT$id");
154
155
                    if(count($alt)) {
156
                        foreach($alt as $ref) {
157
                            // set anchor and backlink for the other footnotes
158
                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
159
                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
160
                        }
161
                    }
162
163
                    // add footnote markup and close this footnote
164
                    $this->doc .= '<div class="content">'.$footnote.'</div>';
165
                    $this->doc .= '</div>'.DOKU_LF;
166
                }
167
            }
168
            $this->doc .= '</div>'.DOKU_LF;
169
        }
170
171
        // Prepare the TOC
172
        global $conf;
173
        if(
174
            $this->info['toc'] &&
175
            is_array($this->toc) &&
176
            $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']
177
        ) {
178
            global $TOC;
179
            $TOC = $this->toc;
180
        }
181
182
        // make sure there are no empty paragraphs
183
        $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
184
    }
185
186
    /**
187
     * Add an item to the TOC
188
     *
189
     * @param string $id       the hash link
190
     * @param string $text     the text to display
191
     * @param int    $level    the nesting level
192
     */
193
    public function toc_additem($id, $text, $level) {
194
        global $conf;
195
196
        //handle TOC
197
        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
198
            $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
199
        }
200
    }
201
202
    /**
203
     * Render a heading
204
     *
205
     * @param string $text  the text to display
206
     * @param int    $level header level
207
     * @param int    $pos   byte position in the original source
208
     */
209
    public function header($text, $level, $pos) {
210
        global $conf;
211
212
        if(blank($text)) return; //skip empty headlines
213
214
        $hid = $this->_headerToLink($text, true);
215
216
        //only add items within configured levels
217
        $this->toc_additem($hid, $text, $level);
218
219
        // adjust $node to reflect hierarchy of levels
220
        $this->node[$level - 1]++;
221
        if($level < $this->lastlevel) {
222
            for($i = 0; $i < $this->lastlevel - $level; $i++) {
223
                $this->node[$this->lastlevel - $i - 1] = 0;
224
            }
225
        }
226
        $this->lastlevel = $level;
227
228
        if($level <= $conf['maxseclevel'] &&
229
            count($this->sectionedits) > 0 &&
230
            $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
231
        ) {
232
            $this->finishSectionEdit($pos - 1);
233
        }
234
235
        // write the header
236
        $this->doc .= DOKU_LF.'<h'.$level;
237
        if($level <= $conf['maxseclevel']) {
238
            $data = array();
239
            $data['target'] = 'section';
240
            $data['name'] = $text;
241
            $data['hid'] = $hid;
242
            $data['codeblockOffset'] = $this->_codeblock;
243
            $this->doc .= ' class="'.$this->startSectionEdit($pos, $data).'"';
244
        }
245
        $this->doc .= ' id="'.$hid.'">';
246
        $this->doc .= $this->_xmlEntities($text);
247
        $this->doc .= "</h$level>".DOKU_LF;
248
    }
249
250
    /**
251
     * Open a new section
252
     *
253
     * @param int $level section level (as determined by the previous header)
254
     */
255
    public function section_open($level) {
256
        $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
257
    }
258
259
    /**
260
     * Close the current section
261
     */
262
    public function section_close() {
263
        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
264
    }
265
266
    /**
267
     * Render plain text data
268
     *
269
     * @param $text
270
     */
271
    public function cdata($text) {
272
        $this->doc .= $this->_xmlEntities($text);
273
    }
274
275
    /**
276
     * Open a paragraph
277
     */
278
    public function p_open() {
279
        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
280
    }
281
282
    /**
283
     * Close a paragraph
284
     */
285
    public function p_close() {
286
        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
287
    }
288
289
    /**
290
     * Create a line break
291
     */
292
    public function linebreak() {
293
        $this->doc .= '<br/>'.DOKU_LF;
294
    }
295
296
    /**
297
     * Create a horizontal line
298
     */
299
    public function hr() {
300
        $this->doc .= '<hr />'.DOKU_LF;
301
    }
302
303
    /**
304
     * Start strong (bold) formatting
305
     */
306
    public function strong_open() {
307
        $this->doc .= '<strong>';
308
    }
309
310
    /**
311
     * Stop strong (bold) formatting
312
     */
313
    public function strong_close() {
314
        $this->doc .= '</strong>';
315
    }
316
317
    /**
318
     * Start emphasis (italics) formatting
319
     */
320
    public function emphasis_open() {
321
        $this->doc .= '<em>';
322
    }
323
324
    /**
325
     * Stop emphasis (italics) formatting
326
     */
327
    public function emphasis_close() {
328
        $this->doc .= '</em>';
329
    }
330
331
    /**
332
     * Start underline formatting
333
     */
334
    public function underline_open() {
335
        $this->doc .= '<em class="u">';
336
    }
337
338
    /**
339
     * Stop underline formatting
340
     */
341
    public function underline_close() {
342
        $this->doc .= '</em>';
343
    }
344
345
    /**
346
     * Start monospace formatting
347
     */
348
    public function monospace_open() {
349
        $this->doc .= '<code>';
350
    }
351
352
    /**
353
     * Stop monospace formatting
354
     */
355
    public function monospace_close() {
356
        $this->doc .= '</code>';
357
    }
358
359
    /**
360
     * Start a subscript
361
     */
362
    public function subscript_open() {
363
        $this->doc .= '<sub>';
364
    }
365
366
    /**
367
     * Stop a subscript
368
     */
369
    public function subscript_close() {
370
        $this->doc .= '</sub>';
371
    }
372
373
    /**
374
     * Start a superscript
375
     */
376
    public function superscript_open() {
377
        $this->doc .= '<sup>';
378
    }
379
380
    /**
381
     * Stop a superscript
382
     */
383
    public function superscript_close() {
384
        $this->doc .= '</sup>';
385
    }
386
387
    /**
388
     * Start deleted (strike-through) formatting
389
     */
390
    public function deleted_open() {
391
        $this->doc .= '<del>';
392
    }
393
394
    /**
395
     * Stop deleted (strike-through) formatting
396
     */
397
    public function deleted_close() {
398
        $this->doc .= '</del>';
399
    }
400
401
    /**
402
     * Callback for footnote start syntax
403
     *
404
     * All following content will go to the footnote instead of
405
     * the document. To achieve this the previous rendered content
406
     * is moved to $store and $doc is cleared
407
     *
408
     * @author Andreas Gohr <[email protected]>
409
     */
410
    public function footnote_open() {
411
412
        // move current content to store and record footnote
413
        $this->store = $this->doc;
414
        $this->doc   = '';
415
    }
416
417
    /**
418
     * Callback for footnote end syntax
419
     *
420
     * All rendered content is moved to the $footnotes array and the old
421
     * content is restored from $store again
422
     *
423
     * @author Andreas Gohr
424
     */
425
    public function footnote_close() {
426
        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
427
        static $fnid = 0;
428
        // assign new footnote id (we start at 1)
429
        $fnid++;
430
431
        // recover footnote into the stack and restore old content
432
        $footnote    = $this->doc;
433
        $this->doc   = $this->store;
434
        $this->store = '';
435
436
        // check to see if this footnote has been seen before
437
        $i = array_search($footnote, $this->footnotes);
438
439
        if($i === false) {
440
            // its a new footnote, add it to the $footnotes array
441
            $this->footnotes[$fnid] = $footnote;
442
        } else {
443
            // seen this one before, save a placeholder
444
            $this->footnotes[$fnid] = "@@FNT".($i);
445
        }
446
447
        // output the footnote reference and link
448
        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
449
    }
450
451
    /**
452
     * Open an unordered list
453
     *
454
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
455
     */
456
    public function listu_open($classes = null) {
457
        $class = '';
458
        if($classes !== null) {
459
            if(is_array($classes)) $classes = join(' ', $classes);
460
            $class = " class=\"$classes\"";
461
        }
462
        $this->doc .= "<ul$class>".DOKU_LF;
463
    }
464
465
    /**
466
     * Close an unordered list
467
     */
468
    public function listu_close() {
469
        $this->doc .= '</ul>'.DOKU_LF;
470
    }
471
472
    /**
473
     * Open an ordered list
474
     *
475
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
476
     */
477
    public function listo_open($classes = null) {
478
        $class = '';
479
        if($classes !== null) {
480
            if(is_array($classes)) $classes = join(' ', $classes);
481
            $class = " class=\"$classes\"";
482
        }
483
        $this->doc .= "<ol$class>".DOKU_LF;
484
    }
485
486
    /**
487
     * Close an ordered list
488
     */
489
    public function listo_close() {
490
        $this->doc .= '</ol>'.DOKU_LF;
491
    }
492
493
    /**
494
     * Open a list item
495
     *
496
     * @param int $level the nesting level
497
     * @param bool $node true when a node; false when a leaf
498
     */
499
    public function listitem_open($level, $node=false) {
500
        $branching = $node ? ' node' : '';
501
        $this->doc .= '<li class="level'.$level.$branching.'">';
502
    }
503
504
    /**
505
     * Close a list item
506
     */
507
    public function listitem_close() {
508
        $this->doc .= '</li>'.DOKU_LF;
509
    }
510
511
    /**
512
     * Start the content of a list item
513
     */
514
    public function listcontent_open() {
515
        $this->doc .= '<div class="li">';
516
    }
517
518
    /**
519
     * Stop the content of a list item
520
     */
521
    public function listcontent_close() {
522
        $this->doc .= '</div>'.DOKU_LF;
523
    }
524
525
    /**
526
     * Output unformatted $text
527
     *
528
     * Defaults to $this->cdata()
529
     *
530
     * @param string $text
531
     */
532
    public function unformatted($text) {
533
        $this->doc .= $this->_xmlEntities($text);
534
    }
535
536
    /**
537
     * Execute PHP code if allowed
538
     *
539
     * @param  string $text      PHP code that is either executed or printed
540
     * @param  string $wrapper   html element to wrap result if $conf['phpok'] is okff
541
     *
542
     * @author Andreas Gohr <[email protected]>
543
     */
544
    public function php($text, $wrapper = 'code') {
545
        global $conf;
546
547
        if($conf['phpok']) {
548
            ob_start();
549
            eval($text);
550
            $this->doc .= ob_get_contents();
551
            ob_end_clean();
552
        } else {
553
            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
554
        }
555
    }
556
557
    /**
558
     * Output block level PHP code
559
     *
560
     * If $conf['phpok'] is true this should evaluate the given code and append the result
561
     * to $doc
562
     *
563
     * @param string $text The PHP code
564
     */
565
    public function phpblock($text) {
566
        $this->php($text, 'pre');
567
    }
568
569
    /**
570
     * Insert HTML if allowed
571
     *
572
     * @param  string $text      html text
573
     * @param  string $wrapper   html element to wrap result if $conf['htmlok'] is okff
574
     *
575
     * @author Andreas Gohr <[email protected]>
576
     */
577
    public function html($text, $wrapper = 'code') {
578
        global $conf;
579
580
        if($conf['htmlok']) {
581
            $this->doc .= $text;
582
        } else {
583
            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
584
        }
585
    }
586
587
    /**
588
     * Output raw block-level HTML
589
     *
590
     * If $conf['htmlok'] is true this should add the code as is to $doc
591
     *
592
     * @param string $text The HTML
593
     */
594
    public function htmlblock($text) {
595
        $this->html($text, 'pre');
596
    }
597
598
    /**
599
     * Start a block quote
600
     */
601
    public function quote_open() {
602
        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
603
    }
604
605
    /**
606
     * Stop a block quote
607
     */
608
    public function quote_close() {
609
        $this->doc .= '</div></blockquote>'.DOKU_LF;
610
    }
611
612
    /**
613
     * Output preformatted text
614
     *
615
     * @param string $text
616
     */
617
    public function preformatted($text) {
618
        $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
619
    }
620
621
    /**
622
     * Display text as file content, optionally syntax highlighted
623
     *
624
     * @param string $text     text to show
625
     * @param string $language programming language to use for syntax highlighting
626
     * @param string $filename file path label
627
     * @param array  $options  assoziative array with additional geshi options
628
     */
629
    public function file($text, $language = null, $filename = null, $options=null) {
630
        $this->_highlight('file', $text, $language, $filename, $options);
631
    }
632
633
    /**
634
     * Display text as code content, optionally syntax highlighted
635
     *
636
     * @param string $text     text to show
637
     * @param string $language programming language to use for syntax highlighting
638
     * @param string $filename file path label
639
     * @param array  $options  assoziative array with additional geshi options
640
     */
641
    public function code($text, $language = null, $filename = null, $options=null) {
642
        $this->_highlight('code', $text, $language, $filename, $options);
643
    }
644
645
    /**
646
     * Use GeSHi to highlight language syntax in code and file blocks
647
     *
648
     * @author Andreas Gohr <[email protected]>
649
     * @param string $type     code|file
650
     * @param string $text     text to show
651
     * @param string $language programming language to use for syntax highlighting
652
     * @param string $filename file path label
653
     * @param array  $options  assoziative array with additional geshi options
654
     */
655
    public function _highlight($type, $text, $language = null, $filename = null, $options = null) {
656
        global $ID;
657
        global $lang;
658
        global $INPUT;
659
660
        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
661
662
        if($filename) {
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
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
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
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
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
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
     * Renders internal and external media
1612
     *
1613
     * @author Andreas Gohr <[email protected]>
1614
     * @param string $src       media ID
1615
     * @param string $title     descriptive text
1616
     * @param string $align     left|center|right
1617
     * @param int    $width     width of media in pixel
1618
     * @param int    $height    height of media in pixel
1619
     * @param string $cache     cache|recache|nocache
1620
     * @param bool   $render    should the media be embedded inline or just linked
1621
     * @return string
1622
     */
1623
    public function _media($src, $title = null, $align = null, $width = null,
1624
                    $height = null, $cache = null, $render = true) {
1625
1626
        $ret = '';
1627
1628
        list($ext, $mime) = mimetype($src);
1629
        if(substr($mime, 0, 5) == 'image') {
1630
            // first get the $title
1631
            if(!is_null($title)) {
1632
                $title = $this->_xmlEntities($title);
1633
            } elseif($ext == 'jpg' || $ext == 'jpeg') {
1634
                //try to use the caption from IPTC/EXIF
1635
                require_once(DOKU_INC.'inc/JpegMeta.php');
1636
                $jpeg = new JpegMeta(mediaFN($src));
1637
                if($jpeg !== false) $cap = $jpeg->getTitle();
1638
                if(!empty($cap)) {
1639
                    $title = $this->_xmlEntities($cap);
1640
                }
1641
            }
1642
            if(!$render) {
1643
                // if the picture is not supposed to be rendered
1644
                // return the title of the picture
1645
                if($title === null || $title === "") {
1646
                    // just show the sourcename
1647
                    $title = $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1648
                }
1649
                return $title;
1650
            }
1651
            //add image tag
1652
            $ret .= '<img src="' . ml(
1653
                    $src,
1654
                    array(
1655
                        'w' => $width, 'h' => $height,
1656
                        'cache' => $cache,
1657
                        'rev' => $this->_getLastMediaRevisionAt($src)
1658
                    )
1659
                ) . '"';
1660
            $ret .= ' class="media'.$align.'"';
1661
            $ret .= ' loading="lazy"';
1662
1663
            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...
1664
                $ret .= ' title="'.$title.'"';
1665
                $ret .= ' alt="'.$title.'"';
1666
            } else {
1667
                $ret .= ' alt=""';
1668
            }
1669
1670
            if(!is_null($width))
1671
                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1672
1673
            if(!is_null($height))
1674
                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1675
1676
            $ret .= ' />';
1677
1678
        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1679
            // first get the $title
1680
            $title = !is_null($title) ? $title : false;
1681
            if(!$render) {
1682
                // if the file is not supposed to be rendered
1683
                // return the title of the file (just the sourcename if there is no title)
1684
                return $this->_xmlEntities($title ? $title : \dokuwiki\Utf8\PhpString::basename(noNS($src)));
1685
            }
1686
1687
            $att          = array();
1688
            $att['class'] = "media$align";
1689
            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...
1690
                $att['title'] = $title;
1691
            }
1692
1693
            if(media_supportedav($mime, 'video')) {
1694
                //add video
1695
                $ret .= $this->_video($src, $width, $height, $att);
1696
            }
1697
            if(media_supportedav($mime, 'audio')) {
1698
                //add audio
1699
                $ret .= $this->_audio($src, $att);
1700
            }
1701
1702
        } elseif($mime == 'application/x-shockwave-flash') {
1703
            if(!$render) {
1704
                // if the flash is not supposed to be rendered
1705
                // return the title of the flash
1706
                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...
1707
                    // just show the sourcename
1708
                    $title = \dokuwiki\Utf8\PhpString::basename(noNS($src));
1709
                }
1710
                return $this->_xmlEntities($title);
1711
            }
1712
1713
            $att          = array();
1714
            $att['class'] = "media$align";
1715
            if($align == 'right') $att['align'] = 'right';
1716
            if($align == 'left') $att['align'] = 'left';
1717
            $ret .= html_flashobject(
1718
                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1719
                array('quality' => 'high'),
1720
                null,
1721
                $att,
1722
                $this->_xmlEntities($title)
1723
            );
1724
        } 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...
1725
            // well at least we have a title to display
1726
            $ret .= $this->_xmlEntities($title);
1727
        } else {
1728
            // just show the sourcename
1729
            $ret .= $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1730
        }
1731
1732
        return $ret;
1733
    }
1734
1735
    /**
1736
     * Escape string for output
1737
     *
1738
     * @param $string
1739
     * @return string
1740
     */
1741
    public function _xmlEntities($string) {
1742
        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1743
    }
1744
1745
1746
1747
    /**
1748
     * Construct a title and handle images in titles
1749
     *
1750
     * @author Harry Fuecks <[email protected]>
1751
     * @param string|array $title    either string title or media array
1752
     * @param string       $default  default title if nothing else is found
1753
     * @param bool         $isImage  will be set to true if it's a media file
1754
     * @param null|string  $id       linked page id (used to extract title from first heading)
1755
     * @param string       $linktype content|navigation
1756
     * @return string      HTML of the title, might be full image tag or just escaped text
1757
     */
1758
    public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1759
        $isImage = false;
1760
        if(is_array($title)) {
1761
            $isImage = true;
1762
            return $this->_imageTitle($title);
1763
        } elseif(is_null($title) || trim($title) == '') {
1764
            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...
1765
                $heading = p_get_first_heading($id);
1766
                if(!blank($heading)) {
1767
                    return $this->_xmlEntities($heading);
1768
                }
1769
            }
1770
            return $this->_xmlEntities($default);
1771
        } else {
1772
            return $this->_xmlEntities($title);
1773
        }
1774
    }
1775
1776
    /**
1777
     * Returns HTML code for images used in link titles
1778
     *
1779
     * @author Andreas Gohr <[email protected]>
1780
     * @param array $img
1781
     * @return string HTML img tag or similar
1782
     */
1783
    public function _imageTitle($img) {
1784
        global $ID;
1785
1786
        // some fixes on $img['src']
1787
        // see internalmedia() and externalmedia()
1788
        list($img['src']) = explode('#', $img['src'], 2);
1789
        if($img['type'] == 'internalmedia') {
1790
            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
0 ignored issues
show
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...
1791
        }
1792
1793
        return $this->_media(
1794
            $img['src'],
1795
            $img['title'],
1796
            $img['align'],
1797
            $img['width'],
1798
            $img['height'],
1799
            $img['cache']
1800
        );
1801
    }
1802
1803
    /**
1804
     * helperfunction to return a basic link to a media
1805
     *
1806
     * used in internalmedia() and externalmedia()
1807
     *
1808
     * @author   Pierre Spring <[email protected]>
1809
     * @param string $src       media ID
1810
     * @param string $title     descriptive text
1811
     * @param string $align     left|center|right
1812
     * @param int    $width     width of media in pixel
1813
     * @param int    $height    height of media in pixel
1814
     * @param string $cache     cache|recache|nocache
1815
     * @param bool   $render    should the media be embedded inline or just linked
1816
     * @return array associative array with link config
1817
     */
1818
    public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1819
        global $conf;
1820
1821
        $link           = array();
1822
        $link['class']  = 'media';
1823
        $link['style']  = '';
1824
        $link['pre']    = '';
1825
        $link['suf']    = '';
1826
        $link['more']   = '';
1827
        $link['target'] = $conf['target']['media'];
1828
        if($conf['target']['media']) $link['rel'] = 'noopener';
1829
        $link['title']  = $this->_xmlEntities($src);
1830
        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1831
1832
        return $link;
1833
    }
1834
1835
    /**
1836
     * Embed video(s) in HTML
1837
     *
1838
     * @author Anika Henke <[email protected]>
1839
     * @author Schplurtz le Déboulonné <[email protected]>
1840
     *
1841
     * @param string $src         - ID of video to embed
1842
     * @param int    $width       - width of the video in pixels
1843
     * @param int    $height      - height of the video in pixels
1844
     * @param array  $atts        - additional attributes for the <video> tag
1845
     * @return string
1846
     */
1847
    public function _video($src, $width, $height, $atts = null) {
1848
        // prepare width and height
1849
        if(is_null($atts)) $atts = array();
1850
        $atts['width']  = (int) $width;
1851
        $atts['height'] = (int) $height;
1852
        if(!$atts['width']) $atts['width'] = 320;
1853
        if(!$atts['height']) $atts['height'] = 240;
1854
1855
        $posterUrl = '';
1856
        $files = array();
1857
        $tracks = array();
1858
        $isExternal = media_isexternal($src);
1859
1860
        if ($isExternal) {
1861
            // take direct source for external files
1862
            list(/*ext*/, $srcMime) = mimetype($src);
1863
            $files[$srcMime] = $src;
1864
        } else {
1865
            // prepare alternative formats
1866
            $extensions   = array('webm', 'ogv', 'mp4');
1867
            $files        = media_alternativefiles($src, $extensions);
1868
            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1869
            $tracks       = media_trackfiles($src);
1870
            if(!empty($poster)) {
1871
                $posterUrl = ml(reset($poster), '', true, '&');
1872
            }
1873
        }
1874
1875
        $out = '';
1876
        // open video tag
1877
        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1878
        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1879
        $out .= '>'.NL;
1880
        $fallback = '';
1881
1882
        // output source for each alternative video format
1883
        foreach($files as $mime => $file) {
1884
            if ($isExternal) {
1885
                $url = $file;
1886
                $linkType = 'externalmedia';
1887
            } else {
1888
                $url = ml($file, '', true, '&');
1889
                $linkType = 'internalmedia';
1890
            }
1891
            $title = !empty($atts['title'])
1892
                ? $atts['title']
1893
                : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1894
1895
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1896
            // alternative content (just a link to the file)
1897
            $fallback .= $this->$linkType(
1898
                $file,
1899
                $title,
1900
                null,
1901
                null,
1902
                null,
1903
                $cache = null,
1904
                $linking = 'linkonly',
1905
                $return = true
1906
            );
1907
        }
1908
1909
        // output each track if any
1910
        foreach( $tracks as $trackid => $info ) {
1911
            list( $kind, $srclang ) = array_map( 'hsc', $info );
1912
            $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
1913
            $out .= "label=\"$srclang\" ";
1914
            $out .= 'src="'.ml($trackid, '', true).'">'.NL;
1915
        }
1916
1917
        // finish
1918
        $out .= $fallback;
1919
        $out .= '</video>'.NL;
1920
        return $out;
1921
    }
1922
1923
    /**
1924
     * Embed audio in HTML
1925
     *
1926
     * @author Anika Henke <[email protected]>
1927
     *
1928
     * @param string $src       - ID of audio to embed
1929
     * @param array  $atts      - additional attributes for the <audio> tag
1930
     * @return string
1931
     */
1932
    public function _audio($src, $atts = array()) {
1933
        $files = array();
1934
        $isExternal = media_isexternal($src);
1935
1936
        if ($isExternal) {
1937
            // take direct source for external files
1938
            list(/*ext*/, $srcMime) = mimetype($src);
1939
            $files[$srcMime] = $src;
1940
        } else {
1941
            // prepare alternative formats
1942
            $extensions   = array('ogg', 'mp3', 'wav');
1943
            $files        = media_alternativefiles($src, $extensions);
1944
        }
1945
1946
        $out = '';
1947
        // open audio tag
1948
        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1949
        $fallback = '';
1950
1951
        // output source for each alternative audio format
1952
        foreach($files as $mime => $file) {
1953
            if ($isExternal) {
1954
                $url = $file;
1955
                $linkType = 'externalmedia';
1956
            } else {
1957
                $url = ml($file, '', true, '&');
1958
                $linkType = 'internalmedia';
1959
            }
1960
            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1961
1962
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1963
            // alternative content (just a link to the file)
1964
            $fallback .= $this->$linkType(
1965
                $file,
1966
                $title,
1967
                null,
1968
                null,
1969
                null,
1970
                $cache = null,
1971
                $linking = 'linkonly',
1972
                $return = true
1973
            );
1974
        }
1975
1976
        // finish
1977
        $out .= $fallback;
1978
        $out .= '</audio>'.NL;
1979
        return $out;
1980
    }
1981
1982
    /**
1983
     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1984
     * which returns an existing media revision less or equal to rev or date_at
1985
     *
1986
     * @author lisps
1987
     * @param string $media_id
1988
     * @access protected
1989
     * @return string revision ('' for current)
1990
     */
1991
    protected function _getLastMediaRevisionAt($media_id){
1992
        if(!$this->date_at || media_isexternal($media_id)) return '';
1993
        $pagelog = new MediaChangeLog($media_id);
1994
        return $pagelog->getLastRevisionAt($this->date_at);
1995
    }
1996
1997
    #endregion
1998
}
1999
2000
//Setup VIM: ex: et ts=4 :
2001