Failed Conditions
Push — psr2-config ( c6639e )
by Andreas
06:39 queued 03:33
created

Doku_Renderer_xhtml::_video()   F

Complexity

Conditions 11
Paths 480

Size

Total Lines 73
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 51
nc 480
nop 4
dl 0
loc 73
rs 3.6144
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Renderer for XHTML output
4
 *
5
 * This is DokuWiki's main renderer used to display page content in the wiki
6
 *
7
 * @author Harry Fuecks <[email protected]>
8
 * @author Andreas Gohr <[email protected]>
9
 *
10
 */
11
class Doku_Renderer_xhtml extends Doku_Renderer {
12
    /** @var array store the table of contents */
13
    public $toc = array();
14
15
    /** @var array A stack of section edit data */
16
    protected $sectionedits = array();
17
18
    /** @var string|int link pages and media against this revision */
19
    public $date_at = '';
20
21
    /** @var int last section edit id, used by startSectionEdit */
22
    protected $lastsecid = 0;
23
24
    /** @var array a list of footnotes, list starts at 1! */
25
    protected $footnotes = array();
26
27
    /** @var int current section level */
28
    protected $lastlevel = 0;
29
    /** @var array section node tracker */
30
    protected $node = array(0, 0, 0, 0, 0);
31
32
    /** @var string temporary $doc store */
33
    protected $store = '';
34
35
    /** @var array global counter, for table classes etc. */
36
    protected $_counter = array(); //
37
38
    /** @var int counts the code and file blocks, used to provide download links */
39
    protected $_codeblock = 0;
40
41
    /** @var array list of allowed URL schemes */
42
    protected $schemes = null;
43
44
    /**
45
     * Register a new edit section range
46
     *
47
     * @param int    $start  The byte position for the edit start
48
     * @param array  $data   Associative array with section data:
49
     *                       Key 'name': the section name/title
50
     *                       Key 'target': the target for the section edit,
51
     *                                     e.g. 'section' or 'table'
52
     *                       Key 'hid': header id
53
     *                       Key 'codeblockOffset': actual code block index
54
     *                       Key 'start': set in startSectionEdit(),
55
     *                                    do not set yourself
56
     *                       Key 'range': calculated from 'start' and
57
     *                                    $key in finishSectionEdit(),
58
     *                                    do not set yourself
59
     * @return string  A marker class for the starting HTML element
60
     *
61
     * @author Adrian Lang <[email protected]>
62
     */
63
    public function startSectionEdit($start, $data) {
64
        if (!is_array($data)) {
65
            msg(
66
                sprintf(
67
                    'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.',
68
                    hsc((string) $data)
69
                ), -1
70
            );
71
72
            // @deprecated 2018-04-14, backward compatibility
73
            $args = func_get_args();
74
            $data = array();
75
            if(isset($args[1])) $data['target'] = $args[1];
76
            if(isset($args[2])) $data['name'] = $args[2];
77
            if(isset($args[3])) $data['hid'] = $args[3];
78
        }
79
        $data['secid'] = ++$this->lastsecid;
80
        $data['start'] = $start;
81
        $this->sectionedits[] = $data;
82
        return 'sectionedit'.$data['secid'];
83
    }
84
85
    /**
86
     * Finish an edit section range
87
     *
88
     * @param int  $end     The byte position for the edit end; null for the rest of the page
89
     *
90
     * @author Adrian Lang <[email protected]>
91
     */
92
    public function finishSectionEdit($end = null, $hid = null) {
93
        $data = array_pop($this->sectionedits);
94
        if(!is_null($end) && $end <= $data['start']) {
95
            return;
96
        }
97
        if(!is_null($hid)) {
98
            $data['hid'] .= $hid;
99
        }
100
        $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end);
101
        unset($data['start']);
102
        $this->doc .= '<!-- EDIT'.hsc(json_encode ($data)).' -->';
103
    }
104
105
    /**
106
     * Returns the format produced by this renderer.
107
     *
108
     * @return string always 'xhtml'
109
     */
110
    public function getFormat() {
111
        return 'xhtml';
112
    }
113
114
    /**
115
     * Initialize the document
116
     */
117
    public function document_start() {
118
        //reset some internals
119
        $this->toc     = array();
120
    }
121
122
    /**
123
     * Finalize the document
124
     */
125
    public function document_end() {
126
        // Finish open section edits.
127
        while(count($this->sectionedits) > 0) {
128
            if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) {
129
                // If there is only one section, do not write a section edit
130
                // marker.
131
                array_pop($this->sectionedits);
132
            } else {
133
                $this->finishSectionEdit();
134
            }
135
        }
136
137
        if(count($this->footnotes) > 0) {
138
            $this->doc .= '<div class="footnotes">'.DOKU_LF;
139
140
            foreach($this->footnotes as $id => $footnote) {
141
                // check its not a placeholder that indicates actual footnote text is elsewhere
142
                if(substr($footnote, 0, 5) != "@@FNT") {
143
144
                    // open the footnote and set the anchor and backlink
145
                    $this->doc .= '<div class="fn">';
146
                    $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">';
147
                    $this->doc .= $id.')</a></sup> '.DOKU_LF;
148
149
                    // get any other footnotes that use the same markup
150
                    $alt = array_keys($this->footnotes, "@@FNT$id");
151
152
                    if(count($alt)) {
153
                        foreach($alt as $ref) {
154
                            // set anchor and backlink for the other footnotes
155
                            $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">';
156
                            $this->doc .= ($ref).')</a></sup> '.DOKU_LF;
157
                        }
158
                    }
159
160
                    // add footnote markup and close this footnote
161
                    $this->doc .= '<div class="content">'.$footnote.'</div>';
162
                    $this->doc .= '</div>'.DOKU_LF;
163
                }
164
            }
165
            $this->doc .= '</div>'.DOKU_LF;
166
        }
167
168
        // Prepare the TOC
169
        global $conf;
170
        if(
171
            $this->info['toc'] &&
172
            is_array($this->toc) &&
173
            $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']
174
        ) {
175
            global $TOC;
176
            $TOC = $this->toc;
177
        }
178
179
        // make sure there are no empty paragraphs
180
        $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc);
181
    }
182
183
    /**
184
     * Add an item to the TOC
185
     *
186
     * @param string $id       the hash link
187
     * @param string $text     the text to display
188
     * @param int    $level    the nesting level
189
     */
190
    public function toc_additem($id, $text, $level) {
191
        global $conf;
192
193
        //handle TOC
194
        if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) {
195
            $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1);
196
        }
197
    }
198
199
    /**
200
     * Render a heading
201
     *
202
     * @param string $text  the text to display
203
     * @param int    $level header level
204
     * @param int    $pos   byte position in the original source
205
     */
206
    public function header($text, $level, $pos) {
207
        global $conf;
208
209
        if(blank($text)) return; //skip empty headlines
210
211
        $hid = $this->_headerToLink($text, true);
212
213
        //only add items within configured levels
214
        $this->toc_additem($hid, $text, $level);
215
216
        // adjust $node to reflect hierarchy of levels
217
        $this->node[$level - 1]++;
218
        if($level < $this->lastlevel) {
219
            for($i = 0; $i < $this->lastlevel - $level; $i++) {
220
                $this->node[$this->lastlevel - $i - 1] = 0;
221
            }
222
        }
223
        $this->lastlevel = $level;
224
225
        if($level <= $conf['maxseclevel'] &&
226
            count($this->sectionedits) > 0 &&
227
            $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section'
228
        ) {
229
            $this->finishSectionEdit($pos - 1);
230
        }
231
232
        // write the header
233
        $this->doc .= DOKU_LF.'<h'.$level;
234
        if($level <= $conf['maxseclevel']) {
235
            $data = array();
236
            $data['target'] = 'section';
237
            $data['name'] = $text;
238
            $data['hid'] = $hid;
239
            $data['codeblockOffset'] = $this->_codeblock;
240
            $this->doc .= ' class="'.$this->startSectionEdit($pos, $data).'"';
241
        }
242
        $this->doc .= ' id="'.$hid.'">';
243
        $this->doc .= $this->_xmlEntities($text);
244
        $this->doc .= "</h$level>".DOKU_LF;
245
    }
246
247
    /**
248
     * Open a new section
249
     *
250
     * @param int $level section level (as determined by the previous header)
251
     */
252
    public function section_open($level) {
253
        $this->doc .= '<div class="level'.$level.'">'.DOKU_LF;
254
    }
255
256
    /**
257
     * Close the current section
258
     */
259
    public function section_close() {
260
        $this->doc .= DOKU_LF.'</div>'.DOKU_LF;
261
    }
262
263
    /**
264
     * Render plain text data
265
     *
266
     * @param $text
267
     */
268
    public function cdata($text) {
269
        $this->doc .= $this->_xmlEntities($text);
270
    }
271
272
    /**
273
     * Open a paragraph
274
     */
275
    public function p_open() {
276
        $this->doc .= DOKU_LF.'<p>'.DOKU_LF;
277
    }
278
279
    /**
280
     * Close a paragraph
281
     */
282
    public function p_close() {
283
        $this->doc .= DOKU_LF.'</p>'.DOKU_LF;
284
    }
285
286
    /**
287
     * Create a line break
288
     */
289
    public function linebreak() {
290
        $this->doc .= '<br/>'.DOKU_LF;
291
    }
292
293
    /**
294
     * Create a horizontal line
295
     */
296
    public function hr() {
297
        $this->doc .= '<hr />'.DOKU_LF;
298
    }
299
300
    /**
301
     * Start strong (bold) formatting
302
     */
303
    public function strong_open() {
304
        $this->doc .= '<strong>';
305
    }
306
307
    /**
308
     * Stop strong (bold) formatting
309
     */
310
    public function strong_close() {
311
        $this->doc .= '</strong>';
312
    }
313
314
    /**
315
     * Start emphasis (italics) formatting
316
     */
317
    public function emphasis_open() {
318
        $this->doc .= '<em>';
319
    }
320
321
    /**
322
     * Stop emphasis (italics) formatting
323
     */
324
    public function emphasis_close() {
325
        $this->doc .= '</em>';
326
    }
327
328
    /**
329
     * Start underline formatting
330
     */
331
    public function underline_open() {
332
        $this->doc .= '<em class="u">';
333
    }
334
335
    /**
336
     * Stop underline formatting
337
     */
338
    public function underline_close() {
339
        $this->doc .= '</em>';
340
    }
341
342
    /**
343
     * Start monospace formatting
344
     */
345
    public function monospace_open() {
346
        $this->doc .= '<code>';
347
    }
348
349
    /**
350
     * Stop monospace formatting
351
     */
352
    public function monospace_close() {
353
        $this->doc .= '</code>';
354
    }
355
356
    /**
357
     * Start a subscript
358
     */
359
    public function subscript_open() {
360
        $this->doc .= '<sub>';
361
    }
362
363
    /**
364
     * Stop a subscript
365
     */
366
    public function subscript_close() {
367
        $this->doc .= '</sub>';
368
    }
369
370
    /**
371
     * Start a superscript
372
     */
373
    public function superscript_open() {
374
        $this->doc .= '<sup>';
375
    }
376
377
    /**
378
     * Stop a superscript
379
     */
380
    public function superscript_close() {
381
        $this->doc .= '</sup>';
382
    }
383
384
    /**
385
     * Start deleted (strike-through) formatting
386
     */
387
    public function deleted_open() {
388
        $this->doc .= '<del>';
389
    }
390
391
    /**
392
     * Stop deleted (strike-through) formatting
393
     */
394
    public function deleted_close() {
395
        $this->doc .= '</del>';
396
    }
397
398
    /**
399
     * Callback for footnote start syntax
400
     *
401
     * All following content will go to the footnote instead of
402
     * the document. To achieve this the previous rendered content
403
     * is moved to $store and $doc is cleared
404
     *
405
     * @author Andreas Gohr <[email protected]>
406
     */
407
    public function footnote_open() {
408
409
        // move current content to store and record footnote
410
        $this->store = $this->doc;
411
        $this->doc   = '';
412
    }
413
414
    /**
415
     * Callback for footnote end syntax
416
     *
417
     * All rendered content is moved to the $footnotes array and the old
418
     * content is restored from $store again
419
     *
420
     * @author Andreas Gohr
421
     */
422
    public function footnote_close() {
423
        /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */
424
        static $fnid = 0;
425
        // assign new footnote id (we start at 1)
426
        $fnid++;
427
428
        // recover footnote into the stack and restore old content
429
        $footnote    = $this->doc;
430
        $this->doc   = $this->store;
431
        $this->store = '';
432
433
        // check to see if this footnote has been seen before
434
        $i = array_search($footnote, $this->footnotes);
435
436
        if($i === false) {
437
            // its a new footnote, add it to the $footnotes array
438
            $this->footnotes[$fnid] = $footnote;
439
        } else {
440
            // seen this one before, save a placeholder
441
            $this->footnotes[$fnid] = "@@FNT".($i);
442
        }
443
444
        // output the footnote reference and link
445
        $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>';
446
    }
447
448
    /**
449
     * Open an unordered list
450
     *
451
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
452
     */
453
    public function listu_open($classes = null) {
454
        $class = '';
455
        if($classes !== null) {
456
            if(is_array($classes)) $classes = join(' ', $classes);
457
            $class = " class=\"$classes\"";
458
        }
459
        $this->doc .= "<ul$class>".DOKU_LF;
460
    }
461
462
    /**
463
     * Close an unordered list
464
     */
465
    public function listu_close() {
466
        $this->doc .= '</ul>'.DOKU_LF;
467
    }
468
469
    /**
470
     * Open an ordered list
471
     *
472
     * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input
473
     */
474
    public function listo_open($classes = null) {
475
        $class = '';
476
        if($classes !== null) {
477
            if(is_array($classes)) $classes = join(' ', $classes);
478
            $class = " class=\"$classes\"";
479
        }
480
        $this->doc .= "<ol$class>".DOKU_LF;
481
    }
482
483
    /**
484
     * Close an ordered list
485
     */
486
    public function listo_close() {
487
        $this->doc .= '</ol>'.DOKU_LF;
488
    }
489
490
    /**
491
     * Open a list item
492
     *
493
     * @param int $level the nesting level
494
     * @param bool $node true when a node; false when a leaf
495
     */
496
    public function listitem_open($level, $node=false) {
497
        $branching = $node ? ' node' : '';
498
        $this->doc .= '<li class="level'.$level.$branching.'">';
499
    }
500
501
    /**
502
     * Close a list item
503
     */
504
    public function listitem_close() {
505
        $this->doc .= '</li>'.DOKU_LF;
506
    }
507
508
    /**
509
     * Start the content of a list item
510
     */
511
    public function listcontent_open() {
512
        $this->doc .= '<div class="li">';
513
    }
514
515
    /**
516
     * Stop the content of a list item
517
     */
518
    public function listcontent_close() {
519
        $this->doc .= '</div>'.DOKU_LF;
520
    }
521
522
    /**
523
     * Output unformatted $text
524
     *
525
     * Defaults to $this->cdata()
526
     *
527
     * @param string $text
528
     */
529
    public function unformatted($text) {
530
        $this->doc .= $this->_xmlEntities($text);
531
    }
532
533
    /**
534
     * Execute PHP code if allowed
535
     *
536
     * @param  string $text      PHP code that is either executed or printed
537
     * @param  string $wrapper   html element to wrap result if $conf['phpok'] is okff
538
     *
539
     * @author Andreas Gohr <[email protected]>
540
     */
541
    public function php($text, $wrapper = 'code') {
542
        global $conf;
543
544
        if($conf['phpok']) {
545
            ob_start();
546
            eval($text);
547
            $this->doc .= ob_get_contents();
548
            ob_end_clean();
549
        } else {
550
            $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper);
551
        }
552
    }
553
554
    /**
555
     * Output block level PHP code
556
     *
557
     * If $conf['phpok'] is true this should evaluate the given code and append the result
558
     * to $doc
559
     *
560
     * @param string $text The PHP code
561
     */
562
    public function phpblock($text) {
563
        $this->php($text, 'pre');
564
    }
565
566
    /**
567
     * Insert HTML if allowed
568
     *
569
     * @param  string $text      html text
570
     * @param  string $wrapper   html element to wrap result if $conf['htmlok'] is okff
571
     *
572
     * @author Andreas Gohr <[email protected]>
573
     */
574
    public function html($text, $wrapper = 'code') {
575
        global $conf;
576
577
        if($conf['htmlok']) {
578
            $this->doc .= $text;
579
        } else {
580
            $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper);
581
        }
582
    }
583
584
    /**
585
     * Output raw block-level HTML
586
     *
587
     * If $conf['htmlok'] is true this should add the code as is to $doc
588
     *
589
     * @param string $text The HTML
590
     */
591
    public function htmlblock($text) {
592
        $this->html($text, 'pre');
593
    }
594
595
    /**
596
     * Start a block quote
597
     */
598
    public function quote_open() {
599
        $this->doc .= '<blockquote><div class="no">'.DOKU_LF;
600
    }
601
602
    /**
603
     * Stop a block quote
604
     */
605
    public function quote_close() {
606
        $this->doc .= '</div></blockquote>'.DOKU_LF;
607
    }
608
609
    /**
610
     * Output preformatted text
611
     *
612
     * @param string $text
613
     */
614
    public function preformatted($text) {
615
        $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF;
616
    }
617
618
    /**
619
     * Display text as file content, optionally syntax highlighted
620
     *
621
     * @param string $text     text to show
622
     * @param string $language programming language to use for syntax highlighting
623
     * @param string $filename file path label
624
     * @param array  $options  assoziative array with additional geshi options
625
     */
626
    public function file($text, $language = null, $filename = null, $options=null) {
627
        $this->_highlight('file', $text, $language, $filename, $options);
628
    }
629
630
    /**
631
     * Display text as code content, optionally syntax highlighted
632
     *
633
     * @param string $text     text to show
634
     * @param string $language programming language to use for syntax highlighting
635
     * @param string $filename file path label
636
     * @param array  $options  assoziative array with additional geshi options
637
     */
638
    public function code($text, $language = null, $filename = null, $options=null) {
639
        $this->_highlight('code', $text, $language, $filename, $options);
640
    }
641
642
    /**
643
     * Use GeSHi to highlight language syntax in code and file blocks
644
     *
645
     * @author Andreas Gohr <[email protected]>
646
     * @param string $type     code|file
647
     * @param string $text     text to show
648
     * @param string $language programming language to use for syntax highlighting
649
     * @param string $filename file path label
650
     * @param array  $options  assoziative array with additional geshi options
651
     */
652
    public function _highlight($type, $text, $language = null, $filename = null, $options = null) {
653
        global $ID;
654
        global $lang;
655
        global $INPUT;
656
657
        $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language);
658
659
        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...
660
            // add icon
661
            list($ext) = mimetype($filename, false);
662
            $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
663
            $class = 'mediafile mf_'.$class;
664
665
            $offset = 0;
666
            if ($INPUT->has('codeblockOffset')) {
667
                $offset = $INPUT->str('codeblockOffset');
668
            }
669
            $this->doc .= '<dl class="'.$type.'">'.DOKU_LF;
670
            $this->doc .= '<dt><a href="' .
671
                exportlink(
672
                    $ID,
673
                    'code',
674
                    array('codeblock' => $offset + $this->_codeblock)
675
                ) . '" title="' . $lang['download'] . '" class="' . $class . '">';
676
            $this->doc .= hsc($filename);
677
            $this->doc .= '</a></dt>'.DOKU_LF.'<dd>';
678
        }
679
680
        if($text{0} == "\n") {
681
            $text = substr($text, 1);
682
        }
683
        if(substr($text, -1) == "\n") {
684
            $text = substr($text, 0, -1);
685
        }
686
687
        if(empty($language)) { // empty is faster than is_null and can prevent '' string
688
            $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF;
689
        } else {
690
            $class = 'code'; //we always need the code class to make the syntax highlighting apply
691
            if($type != 'code') $class .= ' '.$type;
692
693
            $this->doc .= "<pre class=\"$class $language\">" .
694
                p_xhtml_cached_geshi($text, $language, '', $options) .
695
                '</pre>' . DOKU_LF;
696
        }
697
698
        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...
699
            $this->doc .= '</dd></dl>'.DOKU_LF;
700
        }
701
702
        $this->_codeblock++;
703
    }
704
705
    /**
706
     * Format an acronym
707
     *
708
     * Uses $this->acronyms
709
     *
710
     * @param string $acronym
711
     */
712
    public function acronym($acronym) {
713
714
        if(array_key_exists($acronym, $this->acronyms)) {
715
716
            $title = $this->_xmlEntities($this->acronyms[$acronym]);
717
718
            $this->doc .= '<abbr title="'.$title
719
                .'">'.$this->_xmlEntities($acronym).'</abbr>';
720
721
        } else {
722
            $this->doc .= $this->_xmlEntities($acronym);
723
        }
724
    }
725
726
    /**
727
     * Format a smiley
728
     *
729
     * Uses $this->smiley
730
     *
731
     * @param string $smiley
732
     */
733
    public function smiley($smiley) {
734
        if(array_key_exists($smiley, $this->smileys)) {
735
            $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley].
736
                '" class="icon" alt="'.
737
                $this->_xmlEntities($smiley).'" />';
738
        } else {
739
            $this->doc .= $this->_xmlEntities($smiley);
740
        }
741
    }
742
743
    /**
744
     * Format an entity
745
     *
746
     * Entities are basically small text replacements
747
     *
748
     * Uses $this->entities
749
     *
750
     * @param string $entity
751
     */
752
    public function entity($entity) {
753
        if(array_key_exists($entity, $this->entities)) {
754
            $this->doc .= $this->entities[$entity];
755
        } else {
756
            $this->doc .= $this->_xmlEntities($entity);
757
        }
758
    }
759
760
    /**
761
     * Typographically format a multiply sign
762
     *
763
     * Example: ($x=640, $y=480) should result in "640×480"
764
     *
765
     * @param string|int $x first value
766
     * @param string|int $y second value
767
     */
768
    public function multiplyentity($x, $y) {
769
        $this->doc .= "$x&times;$y";
770
    }
771
772
    /**
773
     * Render an opening single quote char (language specific)
774
     */
775
    public function singlequoteopening() {
776
        global $lang;
777
        $this->doc .= $lang['singlequoteopening'];
778
    }
779
780
    /**
781
     * Render a closing single quote char (language specific)
782
     */
783
    public function singlequoteclosing() {
784
        global $lang;
785
        $this->doc .= $lang['singlequoteclosing'];
786
    }
787
788
    /**
789
     * Render an apostrophe char (language specific)
790
     */
791
    public function apostrophe() {
792
        global $lang;
793
        $this->doc .= $lang['apostrophe'];
794
    }
795
796
    /**
797
     * Render an opening double quote char (language specific)
798
     */
799
    public function doublequoteopening() {
800
        global $lang;
801
        $this->doc .= $lang['doublequoteopening'];
802
    }
803
804
    /**
805
     * Render an closinging double quote char (language specific)
806
     */
807
    public function doublequoteclosing() {
808
        global $lang;
809
        $this->doc .= $lang['doublequoteclosing'];
810
    }
811
812
    /**
813
     * Render a CamelCase link
814
     *
815
     * @param string $link       The link name
816
     * @param bool   $returnonly whether to return html or write to doc attribute
817
     * @return void|string writes to doc attribute or returns html depends on $returnonly
818
     *
819
     * @see http://en.wikipedia.org/wiki/CamelCase
820
     */
821
    public function camelcaselink($link, $returnonly = false) {
822
        if($returnonly) {
823
          return $this->internallink($link, $link, null, true);
824
        } else {
825
          $this->internallink($link, $link);
826
        }
827
    }
828
829
    /**
830
     * Render a page local link
831
     *
832
     * @param string $hash       hash link identifier
833
     * @param string $name       name for the link
834
     * @param bool   $returnonly whether to return html or write to doc attribute
835
     * @return void|string writes to doc attribute or returns html depends on $returnonly
836
     */
837
    public function locallink($hash, $name = null, $returnonly = false) {
838
        global $ID;
839
        $name  = $this->_getLinkTitle($name, $hash, $isImage);
840
        $hash  = $this->_headerToLink($hash);
841
        $title = $ID.' ↵';
842
843
        $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">';
844
        $doc .= $name;
845
        $doc .= '</a>';
846
847
        if($returnonly) {
848
          return $doc;
849
        } else {
850
          $this->doc .= $doc;
851
        }
852
    }
853
854
    /**
855
     * Render an internal Wiki Link
856
     *
857
     * $search,$returnonly & $linktype are not for the renderer but are used
858
     * elsewhere - no need to implement them in other renderers
859
     *
860
     * @author Andreas Gohr <[email protected]>
861
     * @param string      $id         pageid
862
     * @param string|null $name       link name
863
     * @param string|null $search     adds search url param
864
     * @param bool        $returnonly whether to return html or write to doc attribute
865
     * @param string      $linktype   type to set use of headings
866
     * @return void|string writes to doc attribute or returns html depends on $returnonly
867
     */
868
    public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') {
869
        global $conf;
870
        global $ID;
871
        global $INFO;
872
873
        $params = '';
874
        $parts  = explode('?', $id, 2);
875
        if(count($parts) === 2) {
876
            $id     = $parts[0];
877
            $params = $parts[1];
878
        }
879
880
        // For empty $id we need to know the current $ID
881
        // We need this check because _simpleTitle needs
882
        // correct $id and resolve_pageid() use cleanID($id)
883
        // (some things could be lost)
884
        if($id === '') {
885
            $id = $ID;
886
        }
887
888
        // default name is based on $id as given
889
        $default = $this->_simpleTitle($id);
890
891
        // now first resolve and clean up the $id
892
        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...
893
894
        $link = array();
895
        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
896
        if(!$isImage) {
897
            if($exists) {
898
                $class = 'wikilink1';
899
            } else {
900
                $class       = 'wikilink2';
901
                $link['rel'] = 'nofollow';
902
            }
903
        } else {
904
            $class = 'media';
905
        }
906
907
        //keep hash anchor
908
        @list($id, $hash) = explode('#', $id, 2);
1 ignored issue
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

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