Failed Conditions
Push — refactorResolving ( 1356e5 )
by Andreas
03:03
created

inc/parser/xhtml.php (3 issues)

Severity

Upgrade to new PHP Analysis Engine

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

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

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
896
897
        $link = array();
898
        $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype);
899
        if(!$isImage) {
900
            if($exists) {
901
                $class = 'wikilink1';
902
            } else {
903
                $class       = 'wikilink2';
904
                $link['rel'] = 'nofollow';
905
            }
906
        } else {
907
            $class = 'media';
908
        }
909
910
        //keep hash anchor
911
        @list($id, $hash) = explode('#', $id, 2);
912
        if(!empty($hash)) $hash = $this->_headerToLink($hash);
913
914
        //prepare for formating
915
        $link['target'] = $conf['target']['wiki'];
916
        $link['style']  = '';
917
        $link['pre']    = '';
918
        $link['suf']    = '';
919
        $link['more']   = 'data-wiki-id="'.$id.'"'; // id is already cleaned
920
        $link['class']  = $class;
921
        if($this->date_at) {
922
            $params = $params.'&at='.rawurlencode($this->date_at);
923
        }
924
        $link['url']    = wl($id, $params);
925
        $link['name']   = $name;
926
        $link['title']  = $id;
927
        //add search string
928
        if($search) {
929
            ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&amp;';
930
            if(is_array($search)) {
931
                $search = array_map('rawurlencode', $search);
932
                $link['url'] .= 's[]='.join('&amp;s[]=', $search);
933
            } else {
934
                $link['url'] .= 's='.rawurlencode($search);
935
            }
936
        }
937
938
        //keep hash
939
        if($hash) $link['url'] .= '#'.$hash;
940
941
        //output formatted
942
        if($returnonly) {
943
            return $this->_formatLink($link);
944
        } else {
945
            $this->doc .= $this->_formatLink($link);
946
        }
947
    }
948
949
    /**
950
     * Render an external link
951
     *
952
     * @param string       $url        full URL with scheme
953
     * @param string|array $name       name for the link, array for media file
954
     * @param bool         $returnonly whether to return html or write to doc attribute
955
     * @return void|string writes to doc attribute or returns html depends on $returnonly
956
     */
957
    public function externallink($url, $name = null, $returnonly = false) {
958
        global $conf;
959
960
        $name = $this->_getLinkTitle($name, $url, $isImage);
961
962
        // url might be an attack vector, only allow registered protocols
963
        if(is_null($this->schemes)) $this->schemes = getSchemes();
964
        list($scheme) = explode('://', $url);
965
        $scheme = strtolower($scheme);
966
        if(!in_array($scheme, $this->schemes)) $url = '';
967
968
        // is there still an URL?
969
        if(!$url) {
970
            if($returnonly) {
971
                return $name;
972
            } else {
973
                $this->doc .= $name;
974
            }
975
            return;
976
        }
977
978
        // set class
979
        if(!$isImage) {
980
            $class = 'urlextern';
981
        } else {
982
            $class = 'media';
983
        }
984
985
        //prepare for formating
986
        $link = array();
987
        $link['target'] = $conf['target']['extern'];
988
        $link['style']  = '';
989
        $link['pre']    = '';
990
        $link['suf']    = '';
991
        $link['more']   = '';
992
        $link['class']  = $class;
993
        $link['url']    = $url;
994
        $link['rel']    = '';
995
996
        $link['name']  = $name;
997
        $link['title'] = $this->_xmlEntities($url);
998
        if($conf['relnofollow']) $link['rel'] .= ' ugc nofollow';
999
        if($conf['target']['extern']) $link['rel'] .= ' noopener';
1000
1001
        //output formatted
1002
        if($returnonly) {
1003
            return $this->_formatLink($link);
1004
        } else {
1005
            $this->doc .= $this->_formatLink($link);
1006
        }
1007
    }
1008
1009
    /**
1010
     * Render an interwiki link
1011
     *
1012
     * You may want to use $this->_resolveInterWiki() here
1013
     *
1014
     * @param string       $match      original link - probably not much use
1015
     * @param string|array $name       name for the link, array for media file
1016
     * @param string       $wikiName   indentifier (shortcut) for the remote wiki
1017
     * @param string       $wikiUri    the fragment parsed from the original link
1018
     * @param bool         $returnonly whether to return html or write to doc attribute
1019
     * @return void|string writes to doc attribute or returns html depends on $returnonly
1020
     */
1021
    public function interwikilink($match, $name, $wikiName, $wikiUri, $returnonly = false) {
1022
        global $conf;
1023
1024
        $link           = array();
1025
        $link['target'] = $conf['target']['interwiki'];
1026
        $link['pre']    = '';
1027
        $link['suf']    = '';
1028
        $link['more']   = '';
1029
        $link['name']   = $this->_getLinkTitle($name, $wikiUri, $isImage);
1030
        $link['rel']    = '';
1031
1032
        //get interwiki URL
1033
        $exists = null;
1034
        $url    = $this->_resolveInterWiki($wikiName, $wikiUri, $exists);
1035
1036
        if(!$isImage) {
1037
            $class         = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName);
1038
            $link['class'] = "interwiki iw_$class";
1039
        } else {
1040
            $link['class'] = 'media';
1041
        }
1042
1043
        //do we stay at the same server? Use local target
1044
        if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) {
1045
            $link['target'] = $conf['target']['wiki'];
1046
        }
1047
        if($exists !== null && !$isImage) {
1048
            if($exists) {
1049
                $link['class'] .= ' wikilink1';
1050
            } else {
1051
                $link['class'] .= ' wikilink2';
1052
                $link['rel'] .= ' nofollow';
1053
            }
1054
        }
1055
        if($conf['target']['interwiki']) $link['rel'] .= ' noopener';
1056
1057
        $link['url']   = $url;
1058
        $link['title'] = htmlspecialchars($link['url']);
1059
1060
        // output formatted
1061
        if($returnonly) {
1062
            if($url == '') return $link['name'];
1063
            return $this->_formatLink($link);
1064
        } else {
1065
            if($url == '') $this->doc .= $link['name'];
1066
            else $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);
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);
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
Deprecated Code introduced by
The function resolve_mediaid() has been deprecated with message: 2020-09-30

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

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
            if($src == '' && empty($title)){
1249
                // make sure at least something will be shown in this case
1250
                $title = $reference;
1251
            }
1252
        }
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
1662
            if($title) {
1663
                $ret .= ' title="'.$title.'"';
1664
                $ret .= ' alt="'.$title.'"';
1665
            } else {
1666
                $ret .= ' alt=""';
1667
            }
1668
1669
            if(!is_null($width))
1670
                $ret .= ' width="'.$this->_xmlEntities($width).'"';
1671
1672
            if(!is_null($height))
1673
                $ret .= ' height="'.$this->_xmlEntities($height).'"';
1674
1675
            $ret .= ' />';
1676
1677
        } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) {
1678
            // first get the $title
1679
            $title = !is_null($title) ? $title : false;
1680
            if(!$render) {
1681
                // if the file is not supposed to be rendered
1682
                // return the title of the file (just the sourcename if there is no title)
1683
                return $this->_xmlEntities($title ? $title : \dokuwiki\Utf8\PhpString::basename(noNS($src)));
1684
            }
1685
1686
            $att          = array();
1687
            $att['class'] = "media$align";
1688
            if($title) {
1689
                $att['title'] = $title;
1690
            }
1691
1692
            if(media_supportedav($mime, 'video')) {
1693
                //add video
1694
                $ret .= $this->_video($src, $width, $height, $att);
1695
            }
1696
            if(media_supportedav($mime, 'audio')) {
1697
                //add audio
1698
                $ret .= $this->_audio($src, $att);
1699
            }
1700
1701
        } elseif($mime == 'application/x-shockwave-flash') {
1702
            if(!$render) {
1703
                // if the flash is not supposed to be rendered
1704
                // return the title of the flash
1705
                if(!$title) {
1706
                    // just show the sourcename
1707
                    $title = \dokuwiki\Utf8\PhpString::basename(noNS($src));
1708
                }
1709
                return $this->_xmlEntities($title);
1710
            }
1711
1712
            $att          = array();
1713
            $att['class'] = "media$align";
1714
            if($align == 'right') $att['align'] = 'right';
1715
            if($align == 'left') $att['align'] = 'left';
1716
            $ret .= html_flashobject(
1717
                ml($src, array('cache' => $cache), true, '&'), $width, $height,
1718
                array('quality' => 'high'),
1719
                null,
1720
                $att,
1721
                $this->_xmlEntities($title)
1722
            );
1723
        } elseif($title) {
1724
            // well at least we have a title to display
1725
            $ret .= $this->_xmlEntities($title);
1726
        } else {
1727
            // just show the sourcename
1728
            $ret .= $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src)));
1729
        }
1730
1731
        return $ret;
1732
    }
1733
1734
    /**
1735
     * Escape string for output
1736
     *
1737
     * @param $string
1738
     * @return string
1739
     */
1740
    public function _xmlEntities($string) {
1741
        return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
1742
    }
1743
1744
1745
1746
    /**
1747
     * Construct a title and handle images in titles
1748
     *
1749
     * @author Harry Fuecks <[email protected]>
1750
     * @param string|array $title    either string title or media array
1751
     * @param string       $default  default title if nothing else is found
1752
     * @param bool         $isImage  will be set to true if it's a media file
1753
     * @param null|string  $id       linked page id (used to extract title from first heading)
1754
     * @param string       $linktype content|navigation
1755
     * @return string      HTML of the title, might be full image tag or just escaped text
1756
     */
1757
    public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') {
1758
        $isImage = false;
1759
        if(is_array($title)) {
1760
            $isImage = true;
1761
            return $this->_imageTitle($title);
1762
        } elseif(is_null($title) || trim($title) == '') {
1763
            if(useHeading($linktype) && $id) {
1764
                $heading = p_get_first_heading($id);
1765
                if(!blank($heading)) {
1766
                    return $this->_xmlEntities($heading);
1767
                }
1768
            }
1769
            return $this->_xmlEntities($default);
1770
        } else {
1771
            return $this->_xmlEntities($title);
1772
        }
1773
    }
1774
1775
    /**
1776
     * Returns HTML code for images used in link titles
1777
     *
1778
     * @author Andreas Gohr <[email protected]>
1779
     * @param array $img
1780
     * @return string HTML img tag or similar
1781
     */
1782
    public function _imageTitle($img) {
1783
        global $ID;
1784
1785
        // some fixes on $img['src']
1786
        // see internalmedia() and externalmedia()
1787
        list($img['src']) = explode('#', $img['src'], 2);
1788
        if($img['type'] == 'internalmedia') {
1789
            resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true);
0 ignored issues
show
Deprecated Code introduced by
The function resolve_mediaid() has been deprecated with message: 2020-09-30

This function has been deprecated. The supplier of the file has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed from the class and what other function to use instead.

Loading history...
1790
        }
1791
1792
        return $this->_media(
1793
            $img['src'],
1794
            $img['title'],
1795
            $img['align'],
1796
            $img['width'],
1797
            $img['height'],
1798
            $img['cache']
1799
        );
1800
    }
1801
1802
    /**
1803
     * helperfunction to return a basic link to a media
1804
     *
1805
     * used in internalmedia() and externalmedia()
1806
     *
1807
     * @author   Pierre Spring <[email protected]>
1808
     * @param string $src       media ID
1809
     * @param string $title     descriptive text
1810
     * @param string $align     left|center|right
1811
     * @param int    $width     width of media in pixel
1812
     * @param int    $height    height of media in pixel
1813
     * @param string $cache     cache|recache|nocache
1814
     * @param bool   $render    should the media be embedded inline or just linked
1815
     * @return array associative array with link config
1816
     */
1817
    public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) {
1818
        global $conf;
1819
1820
        $link           = array();
1821
        $link['class']  = 'media';
1822
        $link['style']  = '';
1823
        $link['pre']    = '';
1824
        $link['suf']    = '';
1825
        $link['more']   = '';
1826
        $link['target'] = $conf['target']['media'];
1827
        if($conf['target']['media']) $link['rel'] = 'noopener';
1828
        $link['title']  = $this->_xmlEntities($src);
1829
        $link['name']   = $this->_media($src, $title, $align, $width, $height, $cache, $render);
1830
1831
        return $link;
1832
    }
1833
1834
    /**
1835
     * Embed video(s) in HTML
1836
     *
1837
     * @author Anika Henke <[email protected]>
1838
     * @author Schplurtz le Déboulonné <[email protected]>
1839
     *
1840
     * @param string $src         - ID of video to embed
1841
     * @param int    $width       - width of the video in pixels
1842
     * @param int    $height      - height of the video in pixels
1843
     * @param array  $atts        - additional attributes for the <video> tag
1844
     * @return string
1845
     */
1846
    public function _video($src, $width, $height, $atts = null) {
1847
        // prepare width and height
1848
        if(is_null($atts)) $atts = array();
1849
        $atts['width']  = (int) $width;
1850
        $atts['height'] = (int) $height;
1851
        if(!$atts['width']) $atts['width'] = 320;
1852
        if(!$atts['height']) $atts['height'] = 240;
1853
1854
        $posterUrl = '';
1855
        $files = array();
1856
        $tracks = array();
1857
        $isExternal = media_isexternal($src);
1858
1859
        if ($isExternal) {
1860
            // take direct source for external files
1861
            list(/*ext*/, $srcMime) = mimetype($src);
1862
            $files[$srcMime] = $src;
1863
        } else {
1864
            // prepare alternative formats
1865
            $extensions   = array('webm', 'ogv', 'mp4');
1866
            $files        = media_alternativefiles($src, $extensions);
1867
            $poster       = media_alternativefiles($src, array('jpg', 'png'));
1868
            $tracks       = media_trackfiles($src);
1869
            if(!empty($poster)) {
1870
                $posterUrl = ml(reset($poster), '', true, '&');
1871
            }
1872
        }
1873
1874
        $out = '';
1875
        // open video tag
1876
        $out .= '<video '.buildAttributes($atts).' controls="controls"';
1877
        if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"';
1878
        $out .= '>'.NL;
1879
        $fallback = '';
1880
1881
        // output source for each alternative video format
1882
        foreach($files as $mime => $file) {
1883
            if ($isExternal) {
1884
                $url = $file;
1885
                $linkType = 'externalmedia';
1886
            } else {
1887
                $url = ml($file, '', true, '&');
1888
                $linkType = 'internalmedia';
1889
            }
1890
            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1891
1892
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1893
            // alternative content (just a link to the file)
1894
            $fallback .= $this->$linkType(
1895
                $file,
1896
                $title,
1897
                null,
1898
                null,
1899
                null,
1900
                $cache = null,
1901
                $linking = 'linkonly',
1902
                $return = true
1903
            );
1904
        }
1905
1906
        // output each track if any
1907
        foreach( $tracks as $trackid => $info ) {
1908
            list( $kind, $srclang ) = array_map( 'hsc', $info );
1909
            $out .= "<track kind=\"$kind\" srclang=\"$srclang\" ";
1910
            $out .= "label=\"$srclang\" ";
1911
            $out .= 'src="'.ml($trackid, '', true).'">'.NL;
1912
        }
1913
1914
        // finish
1915
        $out .= $fallback;
1916
        $out .= '</video>'.NL;
1917
        return $out;
1918
    }
1919
1920
    /**
1921
     * Embed audio in HTML
1922
     *
1923
     * @author Anika Henke <[email protected]>
1924
     *
1925
     * @param string $src       - ID of audio to embed
1926
     * @param array  $atts      - additional attributes for the <audio> tag
1927
     * @return string
1928
     */
1929
    public function _audio($src, $atts = array()) {
1930
        $files = array();
1931
        $isExternal = media_isexternal($src);
1932
1933
        if ($isExternal) {
1934
            // take direct source for external files
1935
            list(/*ext*/, $srcMime) = mimetype($src);
1936
            $files[$srcMime] = $src;
1937
        } else {
1938
            // prepare alternative formats
1939
            $extensions   = array('ogg', 'mp3', 'wav');
1940
            $files        = media_alternativefiles($src, $extensions);
1941
        }
1942
1943
        $out = '';
1944
        // open audio tag
1945
        $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL;
1946
        $fallback = '';
1947
1948
        // output source for each alternative audio format
1949
        foreach($files as $mime => $file) {
1950
            if ($isExternal) {
1951
                $url = $file;
1952
                $linkType = 'externalmedia';
1953
            } else {
1954
                $url = ml($file, '', true, '&');
1955
                $linkType = 'internalmedia';
1956
            }
1957
            $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file)));
1958
1959
            $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL;
1960
            // alternative content (just a link to the file)
1961
            $fallback .= $this->$linkType(
1962
                $file,
1963
                $title,
1964
                null,
1965
                null,
1966
                null,
1967
                $cache = null,
1968
                $linking = 'linkonly',
1969
                $return = true
1970
            );
1971
        }
1972
1973
        // finish
1974
        $out .= $fallback;
1975
        $out .= '</audio>'.NL;
1976
        return $out;
1977
    }
1978
1979
    /**
1980
     * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media()
1981
     * which returns an existing media revision less or equal to rev or date_at
1982
     *
1983
     * @author lisps
1984
     * @param string $media_id
1985
     * @access protected
1986
     * @return string revision ('' for current)
1987
     */
1988
    protected function _getLastMediaRevisionAt($media_id){
1989
        if(!$this->date_at || media_isexternal($media_id)) return '';
1990
        $pagelog = new MediaChangeLog($media_id);
1991
        return $pagelog->getLastRevisionAt($this->date_at);
1992
    }
1993
1994
    #endregion
1995
}
1996
1997
//Setup VIM: ex: et ts=4 :
1998