1<?php 2 3use dokuwiki\ChangeLog\MediaChangeLog; 4use dokuwiki\File\MediaResolver; 5use dokuwiki\File\PageResolver; 6 7/** 8 * Renderer for XHTML output 9 * 10 * This is DokuWiki's main renderer used to display page content in the wiki 11 * 12 * @author Harry Fuecks <hfuecks@gmail.com> 13 * @author Andreas Gohr <andi@splitbrain.org> 14 * 15 */ 16class Doku_Renderer_xhtml extends Doku_Renderer { 17 /** @var array store the table of contents */ 18 public $toc = array(); 19 20 /** @var array A stack of section edit data */ 21 protected $sectionedits = array(); 22 23 /** @var string|int link pages and media against this revision */ 24 public $date_at = ''; 25 26 /** @var int last section edit id, used by startSectionEdit */ 27 protected $lastsecid = 0; 28 29 /** @var array a list of footnotes, list starts at 1! */ 30 protected $footnotes = array(); 31 32 /** @var int current section level */ 33 protected $lastlevel = 0; 34 /** @var array section node tracker */ 35 protected $node = array(0, 0, 0, 0, 0); 36 37 /** @var string temporary $doc store */ 38 protected $store = ''; 39 40 /** @var array global counter, for table classes etc. */ 41 protected $_counter = array(); // 42 43 /** @var int counts the code and file blocks, used to provide download links */ 44 protected $_codeblock = 0; 45 46 /** @var array list of allowed URL schemes */ 47 protected $schemes = null; 48 49 /** 50 * Register a new edit section range 51 * 52 * @param int $start The byte position for the edit start 53 * @param array $data Associative array with section data: 54 * Key 'name': the section name/title 55 * Key 'target': the target for the section edit, 56 * e.g. 'section' or 'table' 57 * Key 'hid': header id 58 * Key 'codeblockOffset': actual code block index 59 * Key 'start': set in startSectionEdit(), 60 * do not set yourself 61 * Key 'range': calculated from 'start' and 62 * $key in finishSectionEdit(), 63 * do not set yourself 64 * @return string A marker class for the starting HTML element 65 * 66 * @author Adrian Lang <lang@cosmocode.de> 67 */ 68 public function startSectionEdit($start, $data) { 69 if (!is_array($data)) { 70 msg( 71 sprintf( 72 'startSectionEdit: $data "%s" is NOT an array! One of your plugins needs an update.', 73 hsc((string) $data) 74 ), -1 75 ); 76 77 // @deprecated 2018-04-14, backward compatibility 78 $args = func_get_args(); 79 $data = array(); 80 if(isset($args[1])) $data['target'] = $args[1]; 81 if(isset($args[2])) $data['name'] = $args[2]; 82 if(isset($args[3])) $data['hid'] = $args[3]; 83 } 84 $data['secid'] = ++$this->lastsecid; 85 $data['start'] = $start; 86 $this->sectionedits[] = $data; 87 return 'sectionedit'.$data['secid']; 88 } 89 90 /** 91 * Finish an edit section range 92 * 93 * @param int $end The byte position for the edit end; null for the rest of the page 94 * 95 * @author Adrian Lang <lang@cosmocode.de> 96 */ 97 public function finishSectionEdit($end = null, $hid = null) { 98 $data = array_pop($this->sectionedits); 99 if(!is_null($end) && $end <= $data['start']) { 100 return; 101 } 102 if(!is_null($hid)) { 103 $data['hid'] .= $hid; 104 } 105 $data['range'] = $data['start'].'-'.(is_null($end) ? '' : $end); 106 unset($data['start']); 107 $this->doc .= '<!-- EDIT'.hsc(json_encode ($data)).' -->'; 108 } 109 110 /** 111 * Returns the format produced by this renderer. 112 * 113 * @return string always 'xhtml' 114 */ 115 public function getFormat() { 116 return 'xhtml'; 117 } 118 119 /** 120 * Initialize the document 121 */ 122 public function document_start() { 123 //reset some internals 124 $this->toc = array(); 125 } 126 127 /** 128 * Finalize the document 129 */ 130 public function document_end() { 131 // Finish open section edits. 132 while(count($this->sectionedits) > 0) { 133 if($this->sectionedits[count($this->sectionedits) - 1]['start'] <= 1) { 134 // If there is only one section, do not write a section edit 135 // marker. 136 array_pop($this->sectionedits); 137 } else { 138 $this->finishSectionEdit(); 139 } 140 } 141 142 if(count($this->footnotes) > 0) { 143 $this->doc .= '<div class="footnotes">'.DOKU_LF; 144 145 foreach($this->footnotes as $id => $footnote) { 146 // check its not a placeholder that indicates actual footnote text is elsewhere 147 if(substr($footnote, 0, 5) != "@@FNT") { 148 149 // open the footnote and set the anchor and backlink 150 $this->doc .= '<div class="fn">'; 151 $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">'; 152 $this->doc .= $id.')</a></sup> '.DOKU_LF; 153 154 // get any other footnotes that use the same markup 155 $alt = array_keys($this->footnotes, "@@FNT$id"); 156 157 if(count($alt)) { 158 foreach($alt as $ref) { 159 // set anchor and backlink for the other footnotes 160 $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">'; 161 $this->doc .= ($ref).')</a></sup> '.DOKU_LF; 162 } 163 } 164 165 // add footnote markup and close this footnote 166 $this->doc .= '<div class="content">'.$footnote.'</div>'; 167 $this->doc .= '</div>'.DOKU_LF; 168 } 169 } 170 $this->doc .= '</div>'.DOKU_LF; 171 } 172 173 // Prepare the TOC 174 global $conf; 175 if( 176 $this->info['toc'] && 177 is_array($this->toc) && 178 $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads'] 179 ) { 180 global $TOC; 181 $TOC = $this->toc; 182 } 183 184 // make sure there are no empty paragraphs 185 $this->doc = preg_replace('#<p>\s*</p>#', '', $this->doc); 186 } 187 188 /** 189 * Add an item to the TOC 190 * 191 * @param string $id the hash link 192 * @param string $text the text to display 193 * @param int $level the nesting level 194 */ 195 public function toc_additem($id, $text, $level) { 196 global $conf; 197 198 //handle TOC 199 if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']) { 200 $this->toc[] = html_mktocitem($id, $text, $level - $conf['toptoclevel'] + 1); 201 } 202 } 203 204 /** 205 * Render a heading 206 * 207 * @param string $text the text to display 208 * @param int $level header level 209 * @param int $pos byte position in the original source 210 */ 211 public function header($text, $level, $pos) { 212 global $conf; 213 214 if(blank($text)) return; //skip empty headlines 215 216 $hid = $this->_headerToLink($text, true); 217 218 //only add items within configured levels 219 $this->toc_additem($hid, $text, $level); 220 221 // adjust $node to reflect hierarchy of levels 222 $this->node[$level - 1]++; 223 if($level < $this->lastlevel) { 224 for($i = 0; $i < $this->lastlevel - $level; $i++) { 225 $this->node[$this->lastlevel - $i - 1] = 0; 226 } 227 } 228 $this->lastlevel = $level; 229 230 if($level <= $conf['maxseclevel'] && 231 count($this->sectionedits) > 0 && 232 $this->sectionedits[count($this->sectionedits) - 1]['target'] === 'section' 233 ) { 234 $this->finishSectionEdit($pos - 1); 235 } 236 237 // write the header 238 $this->doc .= DOKU_LF.'<h'.$level; 239 if($level <= $conf['maxseclevel']) { 240 $data = array(); 241 $data['target'] = 'section'; 242 $data['name'] = $text; 243 $data['hid'] = $hid; 244 $data['codeblockOffset'] = $this->_codeblock; 245 $this->doc .= ' class="'.$this->startSectionEdit($pos, $data).'"'; 246 } 247 $this->doc .= ' id="'.$hid.'">'; 248 $this->doc .= $this->_xmlEntities($text); 249 $this->doc .= "</h$level>".DOKU_LF; 250 } 251 252 /** 253 * Open a new section 254 * 255 * @param int $level section level (as determined by the previous header) 256 */ 257 public function section_open($level) { 258 $this->doc .= '<div class="level'.$level.'">'.DOKU_LF; 259 } 260 261 /** 262 * Close the current section 263 */ 264 public function section_close() { 265 $this->doc .= DOKU_LF.'</div>'.DOKU_LF; 266 } 267 268 /** 269 * Render plain text data 270 * 271 * @param $text 272 */ 273 public function cdata($text) { 274 $this->doc .= $this->_xmlEntities($text); 275 } 276 277 /** 278 * Open a paragraph 279 */ 280 public function p_open() { 281 $this->doc .= DOKU_LF.'<p>'.DOKU_LF; 282 } 283 284 /** 285 * Close a paragraph 286 */ 287 public function p_close() { 288 $this->doc .= DOKU_LF.'</p>'.DOKU_LF; 289 } 290 291 /** 292 * Create a line break 293 */ 294 public function linebreak() { 295 $this->doc .= '<br/>'.DOKU_LF; 296 } 297 298 /** 299 * Create a horizontal line 300 */ 301 public function hr() { 302 $this->doc .= '<hr />'.DOKU_LF; 303 } 304 305 /** 306 * Start strong (bold) formatting 307 */ 308 public function strong_open() { 309 $this->doc .= '<strong>'; 310 } 311 312 /** 313 * Stop strong (bold) formatting 314 */ 315 public function strong_close() { 316 $this->doc .= '</strong>'; 317 } 318 319 /** 320 * Start emphasis (italics) formatting 321 */ 322 public function emphasis_open() { 323 $this->doc .= '<em>'; 324 } 325 326 /** 327 * Stop emphasis (italics) formatting 328 */ 329 public function emphasis_close() { 330 $this->doc .= '</em>'; 331 } 332 333 /** 334 * Start underline formatting 335 */ 336 public function underline_open() { 337 $this->doc .= '<em class="u">'; 338 } 339 340 /** 341 * Stop underline formatting 342 */ 343 public function underline_close() { 344 $this->doc .= '</em>'; 345 } 346 347 /** 348 * Start monospace formatting 349 */ 350 public function monospace_open() { 351 $this->doc .= '<code>'; 352 } 353 354 /** 355 * Stop monospace formatting 356 */ 357 public function monospace_close() { 358 $this->doc .= '</code>'; 359 } 360 361 /** 362 * Start a subscript 363 */ 364 public function subscript_open() { 365 $this->doc .= '<sub>'; 366 } 367 368 /** 369 * Stop a subscript 370 */ 371 public function subscript_close() { 372 $this->doc .= '</sub>'; 373 } 374 375 /** 376 * Start a superscript 377 */ 378 public function superscript_open() { 379 $this->doc .= '<sup>'; 380 } 381 382 /** 383 * Stop a superscript 384 */ 385 public function superscript_close() { 386 $this->doc .= '</sup>'; 387 } 388 389 /** 390 * Start deleted (strike-through) formatting 391 */ 392 public function deleted_open() { 393 $this->doc .= '<del>'; 394 } 395 396 /** 397 * Stop deleted (strike-through) formatting 398 */ 399 public function deleted_close() { 400 $this->doc .= '</del>'; 401 } 402 403 /** 404 * Callback for footnote start syntax 405 * 406 * All following content will go to the footnote instead of 407 * the document. To achieve this the previous rendered content 408 * is moved to $store and $doc is cleared 409 * 410 * @author Andreas Gohr <andi@splitbrain.org> 411 */ 412 public function footnote_open() { 413 414 // move current content to store and record footnote 415 $this->store = $this->doc; 416 $this->doc = ''; 417 } 418 419 /** 420 * Callback for footnote end syntax 421 * 422 * All rendered content is moved to the $footnotes array and the old 423 * content is restored from $store again 424 * 425 * @author Andreas Gohr 426 */ 427 public function footnote_close() { 428 /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */ 429 static $fnid = 0; 430 // assign new footnote id (we start at 1) 431 $fnid++; 432 433 // recover footnote into the stack and restore old content 434 $footnote = $this->doc; 435 $this->doc = $this->store; 436 $this->store = ''; 437 438 // check to see if this footnote has been seen before 439 $i = array_search($footnote, $this->footnotes); 440 441 if($i === false) { 442 // its a new footnote, add it to the $footnotes array 443 $this->footnotes[$fnid] = $footnote; 444 } else { 445 // seen this one before, save a placeholder 446 $this->footnotes[$fnid] = "@@FNT".($i); 447 } 448 449 // output the footnote reference and link 450 $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>'; 451 } 452 453 /** 454 * Open an unordered list 455 * 456 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 457 */ 458 public function listu_open($classes = null) { 459 $class = ''; 460 if($classes !== null) { 461 if(is_array($classes)) $classes = join(' ', $classes); 462 $class = " class=\"$classes\""; 463 } 464 $this->doc .= "<ul$class>".DOKU_LF; 465 } 466 467 /** 468 * Close an unordered list 469 */ 470 public function listu_close() { 471 $this->doc .= '</ul>'.DOKU_LF; 472 } 473 474 /** 475 * Open an ordered list 476 * 477 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 478 */ 479 public function listo_open($classes = null) { 480 $class = ''; 481 if($classes !== null) { 482 if(is_array($classes)) $classes = join(' ', $classes); 483 $class = " class=\"$classes\""; 484 } 485 $this->doc .= "<ol$class>".DOKU_LF; 486 } 487 488 /** 489 * Close an ordered list 490 */ 491 public function listo_close() { 492 $this->doc .= '</ol>'.DOKU_LF; 493 } 494 495 /** 496 * Open a list item 497 * 498 * @param int $level the nesting level 499 * @param bool $node true when a node; false when a leaf 500 */ 501 public function listitem_open($level, $node=false) { 502 $branching = $node ? ' node' : ''; 503 $this->doc .= '<li class="level'.$level.$branching.'">'; 504 } 505 506 /** 507 * Close a list item 508 */ 509 public function listitem_close() { 510 $this->doc .= '</li>'.DOKU_LF; 511 } 512 513 /** 514 * Start the content of a list item 515 */ 516 public function listcontent_open() { 517 $this->doc .= '<div class="li">'; 518 } 519 520 /** 521 * Stop the content of a list item 522 */ 523 public function listcontent_close() { 524 $this->doc .= '</div>'.DOKU_LF; 525 } 526 527 /** 528 * Output unformatted $text 529 * 530 * Defaults to $this->cdata() 531 * 532 * @param string $text 533 */ 534 public function unformatted($text) { 535 $this->doc .= $this->_xmlEntities($text); 536 } 537 538 /** 539 * Execute PHP code if allowed 540 * 541 * @param string $text PHP code that is either executed or printed 542 * @param string $wrapper html element to wrap result if $conf['phpok'] is okff 543 * 544 * @author Andreas Gohr <andi@splitbrain.org> 545 */ 546 public function php($text, $wrapper = 'code') { 547 global $conf; 548 549 if($conf['phpok']) { 550 ob_start(); 551 eval($text); 552 $this->doc .= ob_get_contents(); 553 ob_end_clean(); 554 } else { 555 $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper); 556 } 557 } 558 559 /** 560 * Output block level PHP code 561 * 562 * If $conf['phpok'] is true this should evaluate the given code and append the result 563 * to $doc 564 * 565 * @param string $text The PHP code 566 */ 567 public function phpblock($text) { 568 $this->php($text, 'pre'); 569 } 570 571 /** 572 * Insert HTML if allowed 573 * 574 * @param string $text html text 575 * @param string $wrapper html element to wrap result if $conf['htmlok'] is okff 576 * 577 * @author Andreas Gohr <andi@splitbrain.org> 578 */ 579 public function html($text, $wrapper = 'code') { 580 global $conf; 581 582 if($conf['htmlok']) { 583 $this->doc .= $text; 584 } else { 585 $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper); 586 } 587 } 588 589 /** 590 * Output raw block-level HTML 591 * 592 * If $conf['htmlok'] is true this should add the code as is to $doc 593 * 594 * @param string $text The HTML 595 */ 596 public function htmlblock($text) { 597 $this->html($text, 'pre'); 598 } 599 600 /** 601 * Start a block quote 602 */ 603 public function quote_open() { 604 $this->doc .= '<blockquote><div class="no">'.DOKU_LF; 605 } 606 607 /** 608 * Stop a block quote 609 */ 610 public function quote_close() { 611 $this->doc .= '</div></blockquote>'.DOKU_LF; 612 } 613 614 /** 615 * Output preformatted text 616 * 617 * @param string $text 618 */ 619 public function preformatted($text) { 620 $this->doc .= '<pre class="code">'.trim($this->_xmlEntities($text), "\n\r").'</pre>'.DOKU_LF; 621 } 622 623 /** 624 * Display text as file content, optionally syntax highlighted 625 * 626 * @param string $text text to show 627 * @param string $language programming language to use for syntax highlighting 628 * @param string $filename file path label 629 * @param array $options assoziative array with additional geshi options 630 */ 631 public function file($text, $language = null, $filename = null, $options=null) { 632 $this->_highlight('file', $text, $language, $filename, $options); 633 } 634 635 /** 636 * Display text as code content, optionally syntax highlighted 637 * 638 * @param string $text text to show 639 * @param string $language programming language to use for syntax highlighting 640 * @param string $filename file path label 641 * @param array $options assoziative array with additional geshi options 642 */ 643 public function code($text, $language = null, $filename = null, $options=null) { 644 $this->_highlight('code', $text, $language, $filename, $options); 645 } 646 647 /** 648 * Use GeSHi to highlight language syntax in code and file blocks 649 * 650 * @author Andreas Gohr <andi@splitbrain.org> 651 * @param string $type code|file 652 * @param string $text text to show 653 * @param string $language programming language to use for syntax highlighting 654 * @param string $filename file path label 655 * @param array $options assoziative array with additional geshi options 656 */ 657 public function _highlight($type, $text, $language = null, $filename = null, $options = null) { 658 global $ID; 659 global $lang; 660 global $INPUT; 661 662 $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language ?? ''); 663 664 if($filename) { 665 // add icon 666 list($ext) = mimetype($filename, false); 667 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 668 $class = 'mediafile mf_'.$class; 669 670 $offset = 0; 671 if ($INPUT->has('codeblockOffset')) { 672 $offset = $INPUT->str('codeblockOffset'); 673 } 674 $this->doc .= '<dl class="'.$type.'">'.DOKU_LF; 675 $this->doc .= '<dt><a href="' . 676 exportlink( 677 $ID, 678 'code', 679 array('codeblock' => $offset + $this->_codeblock) 680 ) . '" title="' . $lang['download'] . '" class="' . $class . '">'; 681 $this->doc .= hsc($filename); 682 $this->doc .= '</a></dt>'.DOKU_LF.'<dd>'; 683 } 684 685 if($text[0] == "\n") { 686 $text = substr($text, 1); 687 } 688 if(substr($text, -1) == "\n") { 689 $text = substr($text, 0, -1); 690 } 691 692 if(empty($language)) { // empty is faster than is_null and can prevent '' string 693 $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF; 694 } else { 695 $class = 'code'; //we always need the code class to make the syntax highlighting apply 696 if($type != 'code') $class .= ' '.$type; 697 698 $this->doc .= "<pre class=\"$class $language\">" . 699 p_xhtml_cached_geshi($text, $language, '', $options) . 700 '</pre>' . DOKU_LF; 701 } 702 703 if($filename) { 704 $this->doc .= '</dd></dl>'.DOKU_LF; 705 } 706 707 $this->_codeblock++; 708 } 709 710 /** 711 * Format an acronym 712 * 713 * Uses $this->acronyms 714 * 715 * @param string $acronym 716 */ 717 public function acronym($acronym) { 718 719 if(array_key_exists($acronym, $this->acronyms)) { 720 721 $title = $this->_xmlEntities($this->acronyms[$acronym]); 722 723 $this->doc .= '<abbr title="'.$title 724 .'">'.$this->_xmlEntities($acronym).'</abbr>'; 725 726 } else { 727 $this->doc .= $this->_xmlEntities($acronym); 728 } 729 } 730 731 /** 732 * Format a smiley 733 * 734 * Uses $this->smiley 735 * 736 * @param string $smiley 737 */ 738 public function smiley($smiley) { 739 if (isset($this->smileys[$smiley])) { 740 $this->doc .= '<img src="' . DOKU_BASE . 'lib/images/smileys/' . $this->smileys[$smiley] . 741 '" class="icon smiley" alt="' . $this->_xmlEntities($smiley) . '" />'; 742 } else { 743 $this->doc .= $this->_xmlEntities($smiley); 744 } 745 } 746 747 /** 748 * Format an entity 749 * 750 * Entities are basically small text replacements 751 * 752 * Uses $this->entities 753 * 754 * @param string $entity 755 */ 756 public function entity($entity) { 757 if(array_key_exists($entity, $this->entities)) { 758 $this->doc .= $this->entities[$entity]; 759 } else { 760 $this->doc .= $this->_xmlEntities($entity); 761 } 762 } 763 764 /** 765 * Typographically format a multiply sign 766 * 767 * Example: ($x=640, $y=480) should result in "640×480" 768 * 769 * @param string|int $x first value 770 * @param string|int $y second value 771 */ 772 public function multiplyentity($x, $y) { 773 $this->doc .= "$x×$y"; 774 } 775 776 /** 777 * Render an opening single quote char (language specific) 778 */ 779 public function singlequoteopening() { 780 global $lang; 781 $this->doc .= $lang['singlequoteopening']; 782 } 783 784 /** 785 * Render a closing single quote char (language specific) 786 */ 787 public function singlequoteclosing() { 788 global $lang; 789 $this->doc .= $lang['singlequoteclosing']; 790 } 791 792 /** 793 * Render an apostrophe char (language specific) 794 */ 795 public function apostrophe() { 796 global $lang; 797 $this->doc .= $lang['apostrophe']; 798 } 799 800 /** 801 * Render an opening double quote char (language specific) 802 */ 803 public function doublequoteopening() { 804 global $lang; 805 $this->doc .= $lang['doublequoteopening']; 806 } 807 808 /** 809 * Render an closinging double quote char (language specific) 810 */ 811 public function doublequoteclosing() { 812 global $lang; 813 $this->doc .= $lang['doublequoteclosing']; 814 } 815 816 /** 817 * Render a CamelCase link 818 * 819 * @param string $link The link name 820 * @param bool $returnonly whether to return html or write to doc attribute 821 * @return void|string writes to doc attribute or returns html depends on $returnonly 822 * 823 * @see http://en.wikipedia.org/wiki/CamelCase 824 */ 825 public function camelcaselink($link, $returnonly = false) { 826 if($returnonly) { 827 return $this->internallink($link, $link, null, true); 828 } else { 829 $this->internallink($link, $link); 830 } 831 } 832 833 /** 834 * Render a page local link 835 * 836 * @param string $hash hash link identifier 837 * @param string $name name for the link 838 * @param bool $returnonly whether to return html or write to doc attribute 839 * @return void|string writes to doc attribute or returns html depends on $returnonly 840 */ 841 public function locallink($hash, $name = null, $returnonly = false) { 842 global $ID; 843 $name = $this->_getLinkTitle($name, $hash, $isImage); 844 $hash = $this->_headerToLink($hash); 845 $title = $ID.' ↵'; 846 847 $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">'; 848 $doc .= $name; 849 $doc .= '</a>'; 850 851 if($returnonly) { 852 return $doc; 853 } else { 854 $this->doc .= $doc; 855 } 856 } 857 858 /** 859 * Render an internal Wiki Link 860 * 861 * $search,$returnonly & $linktype are not for the renderer but are used 862 * elsewhere - no need to implement them in other renderers 863 * 864 * @author Andreas Gohr <andi@splitbrain.org> 865 * @param string $id pageid 866 * @param string|null $name link name 867 * @param string|null $search adds search url param 868 * @param bool $returnonly whether to return html or write to doc attribute 869 * @param string $linktype type to set use of headings 870 * @return void|string writes to doc attribute or returns html depends on $returnonly 871 */ 872 public function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') { 873 global $conf; 874 global $ID; 875 global $INFO; 876 877 $params = ''; 878 $parts = explode('?', $id, 2); 879 if(count($parts) === 2) { 880 $id = $parts[0]; 881 $params = $parts[1]; 882 } 883 884 // For empty $id we need to know the current $ID 885 // We need this check because _simpleTitle needs 886 // correct $id and resolve_pageid() use cleanID($id) 887 // (some things could be lost) 888 if($id === '') { 889 $id = $ID; 890 } 891 892 // default name is based on $id as given 893 $default = $this->_simpleTitle($id); 894 895 // now first resolve and clean up the $id 896 $id = (new PageResolver($ID))->resolveId($id, $this->date_at, true); 897 $exists = page_exists($id, $this->date_at, false, true); 898 899 $link = array(); 900 $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype); 901 if(!$isImage) { 902 if($exists) { 903 $class = 'wikilink1'; 904 } else { 905 $class = 'wikilink2'; 906 $link['rel'] = 'nofollow'; 907 } 908 } else { 909 $class = 'media'; 910 } 911 912 //keep hash anchor 913 @list($id, $hash) = explode('#', $id, 2); 914 if(!empty($hash)) $hash = $this->_headerToLink($hash); 915 916 //prepare for formating 917 $link['target'] = $conf['target']['wiki']; 918 $link['style'] = ''; 919 $link['pre'] = ''; 920 $link['suf'] = ''; 921 $link['more'] = 'data-wiki-id="'.$id.'"'; // id is already cleaned 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) { 931 ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&'; 932 if(is_array($search)) { 933 $search = array_map('rawurlencode', $search); 934 $link['url'] .= 's[]='.join('&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); 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'] .= ' ugc 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'] = $this->_xmlEntities($link['url']); 1061 1062 // output formatted 1063 if($returnonly) { 1064 if($url == '') return $link['name']; 1065 return $this->_formatLink($link); 1066 } else { 1067 if($url == '') $this->doc .= $link['name']; 1068 else $this->doc .= $this->_formatLink($link); 1069 } 1070 } 1071 1072 /** 1073 * Link to windows share 1074 * 1075 * @param string $url the link 1076 * @param string|array $name name for the link, array for media file 1077 * @param bool $returnonly whether to return html or write to doc attribute 1078 * @return void|string writes to doc attribute or returns html depends on $returnonly 1079 */ 1080 public function windowssharelink($url, $name = null, $returnonly = false) { 1081 global $conf; 1082 1083 //simple setup 1084 $link = array(); 1085 $link['target'] = $conf['target']['windows']; 1086 $link['pre'] = ''; 1087 $link['suf'] = ''; 1088 $link['style'] = ''; 1089 1090 $link['name'] = $this->_getLinkTitle($name, $url, $isImage); 1091 if(!$isImage) { 1092 $link['class'] = 'windows'; 1093 } else { 1094 $link['class'] = 'media'; 1095 } 1096 1097 $link['title'] = $this->_xmlEntities($url); 1098 $url = str_replace('\\', '/', $url); 1099 $url = 'file:///'.$url; 1100 $link['url'] = $url; 1101 1102 //output formatted 1103 if($returnonly) { 1104 return $this->_formatLink($link); 1105 } else { 1106 $this->doc .= $this->_formatLink($link); 1107 } 1108 } 1109 1110 /** 1111 * Render a linked E-Mail Address 1112 * 1113 * Honors $conf['mailguard'] setting 1114 * 1115 * @param string $address Email-Address 1116 * @param string|array $name name for the link, array for media file 1117 * @param bool $returnonly whether to return html or write to doc attribute 1118 * @return void|string writes to doc attribute or returns html depends on $returnonly 1119 */ 1120 public function emaillink($address, $name = null, $returnonly = false) { 1121 global $conf; 1122 //simple setup 1123 $link = array(); 1124 $link['target'] = ''; 1125 $link['pre'] = ''; 1126 $link['suf'] = ''; 1127 $link['style'] = ''; 1128 $link['more'] = ''; 1129 1130 $name = $this->_getLinkTitle($name, '', $isImage); 1131 if(!$isImage) { 1132 $link['class'] = 'mail'; 1133 } else { 1134 $link['class'] = 'media'; 1135 } 1136 1137 $address = $this->_xmlEntities($address); 1138 $address = obfuscate($address); 1139 $title = $address; 1140 1141 if(empty($name)) { 1142 $name = $address; 1143 } 1144 1145 if($conf['mailguard'] == 'visible') $address = rawurlencode($address); 1146 1147 $link['url'] = 'mailto:'.$address; 1148 $link['name'] = $name; 1149 $link['title'] = $title; 1150 1151 //output formatted 1152 if($returnonly) { 1153 return $this->_formatLink($link); 1154 } else { 1155 $this->doc .= $this->_formatLink($link); 1156 } 1157 } 1158 1159 /** 1160 * Render an internal media file 1161 * 1162 * @param string $src media ID 1163 * @param string $title descriptive text 1164 * @param string $align left|center|right 1165 * @param int $width width of media in pixel 1166 * @param int $height height of media in pixel 1167 * @param string $cache cache|recache|nocache 1168 * @param string $linking linkonly|detail|nolink 1169 * @param bool $return return HTML instead of adding to $doc 1170 * @return void|string writes to doc attribute or returns html depends on $return 1171 */ 1172 public function internalmedia($src, $title = null, $align = null, $width = null, 1173 $height = null, $cache = null, $linking = null, $return = false) { 1174 global $ID; 1175 if (strpos($src, '#') !== false) { 1176 list($src, $hash) = explode('#', $src, 2); 1177 } 1178 $src = (new MediaResolver($ID))->resolveId($src,$this->date_at,true); 1179 $exists = media_exists($src); 1180 1181 $noLink = false; 1182 $render = ($linking == 'linkonly') ? false : true; 1183 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 1184 1185 list($ext, $mime) = mimetype($src, false); 1186 if(substr($mime, 0, 5) == 'image' && $render) { 1187 $link['url'] = ml( 1188 $src, 1189 array( 1190 'id' => $ID, 1191 'cache' => $cache, 1192 'rev' => $this->_getLastMediaRevisionAt($src) 1193 ), 1194 ($linking == 'direct') 1195 ); 1196 } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) { 1197 // don't link movies 1198 $noLink = true; 1199 } else { 1200 // add file icons 1201 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 1202 $link['class'] .= ' mediafile mf_'.$class; 1203 $link['url'] = ml( 1204 $src, 1205 array( 1206 'id' => $ID, 1207 'cache' => $cache, 1208 'rev' => $this->_getLastMediaRevisionAt($src) 1209 ), 1210 true 1211 ); 1212 if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')'; 1213 } 1214 1215 if (!empty($hash)) $link['url'] .= '#'.$hash; 1216 1217 //markup non existing files 1218 if(!$exists) { 1219 $link['class'] .= ' wikilink2'; 1220 } 1221 1222 //output formatted 1223 if($return) { 1224 if($linking == 'nolink' || $noLink) return $link['name']; 1225 else return $this->_formatLink($link); 1226 } else { 1227 if($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 1228 else $this->doc .= $this->_formatLink($link); 1229 } 1230 } 1231 1232 /** 1233 * Render an external media file 1234 * 1235 * @param string $src full media URL 1236 * @param string $title descriptive text 1237 * @param string $align left|center|right 1238 * @param int $width width of media in pixel 1239 * @param int $height height of media in pixel 1240 * @param string $cache cache|recache|nocache 1241 * @param string $linking linkonly|detail|nolink 1242 * @param bool $return return HTML instead of adding to $doc 1243 * @return void|string writes to doc attribute or returns html depends on $return 1244 */ 1245 public function externalmedia($src, $title = null, $align = null, $width = null, 1246 $height = null, $cache = null, $linking = null, $return = false) { 1247 if(link_isinterwiki($src)){ 1248 list($shortcut, $reference) = explode('>', $src, 2); 1249 $exists = null; 1250 $src = $this->_resolveInterWiki($shortcut, $reference, $exists); 1251 if($src == '' && empty($title)){ 1252 // make sure at least something will be shown in this case 1253 $title = $reference; 1254 } 1255 } 1256 // Squelch the warning in case there is no hash in the URL 1257 @list($src, $hash) = explode('#', $src, 2); 1258 $noLink = false; 1259 if($src == '') { 1260 // only output plaintext without link if there is no src 1261 $noLink = true; 1262 } 1263 $render = ($linking == 'linkonly') ? false : true; 1264 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 1265 1266 $link['url'] = ml($src, array('cache' => $cache)); 1267 1268 list($ext, $mime) = mimetype($src, false); 1269 if(substr($mime, 0, 5) == 'image' && $render) { 1270 // link only jpeg images 1271 // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true; 1272 } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) { 1273 // don't link movies 1274 $noLink = true; 1275 } else { 1276 // add file icons 1277 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 1278 $link['class'] .= ' mediafile mf_'.$class; 1279 } 1280 1281 if($hash) $link['url'] .= '#'.$hash; 1282 1283 //output formatted 1284 if($return) { 1285 if($linking == 'nolink' || $noLink) return $link['name']; 1286 else return $this->_formatLink($link); 1287 } else { 1288 if($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 1289 else $this->doc .= $this->_formatLink($link); 1290 } 1291 } 1292 1293 /** 1294 * Renders an RSS feed 1295 * 1296 * @param string $url URL of the feed 1297 * @param array $params Finetuning of the output 1298 * 1299 * @author Andreas Gohr <andi@splitbrain.org> 1300 */ 1301 public function rss($url, $params) { 1302 global $lang; 1303 global $conf; 1304 1305 require_once(DOKU_INC.'inc/FeedParser.php'); 1306 $feed = new FeedParser(); 1307 $feed->set_feed_url($url); 1308 1309 //disable warning while fetching 1310 if(!defined('DOKU_E_LEVEL')) { 1311 $elvl = error_reporting(E_ERROR); 1312 } 1313 $rc = $feed->init(); 1314 if(isset($elvl)) { 1315 error_reporting($elvl); 1316 } 1317 1318 if($params['nosort']) $feed->enable_order_by_date(false); 1319 1320 //decide on start and end 1321 if($params['reverse']) { 1322 $mod = -1; 1323 $start = $feed->get_item_quantity() - 1; 1324 $end = $start - ($params['max']); 1325 $end = ($end < -1) ? -1 : $end; 1326 } else { 1327 $mod = 1; 1328 $start = 0; 1329 $end = $feed->get_item_quantity(); 1330 $end = ($end > $params['max']) ? $params['max'] : $end; 1331 } 1332 1333 $this->doc .= '<ul class="rss">'; 1334 if($rc) { 1335 for($x = $start; $x != $end; $x += $mod) { 1336 $item = $feed->get_item($x); 1337 $this->doc .= '<li><div class="li">'; 1338 // support feeds without links 1339 $lnkurl = $item->get_permalink(); 1340 if($lnkurl) { 1341 // title is escaped by SimplePie, we unescape here because it 1342 // is escaped again in externallink() FS#1705 1343 $this->externallink( 1344 $item->get_permalink(), 1345 html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8') 1346 ); 1347 } else { 1348 $this->doc .= ' '.$item->get_title(); 1349 } 1350 if($params['author']) { 1351 $author = $item->get_author(0); 1352 if($author) { 1353 $name = $author->get_name(); 1354 if(!$name) $name = $author->get_email(); 1355 if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name); 1356 } 1357 } 1358 if($params['date']) { 1359 $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')'; 1360 } 1361 if($params['details']) { 1362 $this->doc .= '<div class="detail">'; 1363 if($conf['htmlok']) { 1364 $this->doc .= $item->get_description(); 1365 } else { 1366 $this->doc .= strip_tags($item->get_description()); 1367 } 1368 $this->doc .= '</div>'; 1369 } 1370 1371 $this->doc .= '</div></li>'; 1372 } 1373 } else { 1374 $this->doc .= '<li><div class="li">'; 1375 $this->doc .= '<em>'.$lang['rssfailed'].'</em>'; 1376 $this->externallink($url); 1377 if($conf['allowdebug']) { 1378 $this->doc .= '<!--'.hsc($feed->error).'-->'; 1379 } 1380 $this->doc .= '</div></li>'; 1381 } 1382 $this->doc .= '</ul>'; 1383 } 1384 1385 /** 1386 * Start a table 1387 * 1388 * @param int $maxcols maximum number of columns 1389 * @param int $numrows NOT IMPLEMENTED 1390 * @param int $pos byte position in the original source 1391 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1392 */ 1393 public function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) { 1394 // initialize the row counter used for classes 1395 $this->_counter['row_counter'] = 0; 1396 $class = 'table'; 1397 if($classes !== null) { 1398 if(is_array($classes)) $classes = join(' ', $classes); 1399 $class .= ' ' . $classes; 1400 } 1401 if($pos !== null) { 1402 $hid = $this->_headerToLink($class, true); 1403 $data = array(); 1404 $data['target'] = 'table'; 1405 $data['name'] = ''; 1406 $data['hid'] = $hid; 1407 $class .= ' '.$this->startSectionEdit($pos, $data); 1408 } 1409 $this->doc .= '<div class="'.$class.'"><table class="inline">'. 1410 DOKU_LF; 1411 } 1412 1413 /** 1414 * Close a table 1415 * 1416 * @param int $pos byte position in the original source 1417 */ 1418 public function table_close($pos = null) { 1419 $this->doc .= '</table></div>'.DOKU_LF; 1420 if($pos !== null) { 1421 $this->finishSectionEdit($pos); 1422 } 1423 } 1424 1425 /** 1426 * Open a table header 1427 */ 1428 public function tablethead_open() { 1429 $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF; 1430 } 1431 1432 /** 1433 * Close a table header 1434 */ 1435 public function tablethead_close() { 1436 $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF; 1437 } 1438 1439 /** 1440 * Open a table body 1441 */ 1442 public function tabletbody_open() { 1443 $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF; 1444 } 1445 1446 /** 1447 * Close a table body 1448 */ 1449 public function tabletbody_close() { 1450 $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF; 1451 } 1452 1453 /** 1454 * Open a table footer 1455 */ 1456 public function tabletfoot_open() { 1457 $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF; 1458 } 1459 1460 /** 1461 * Close a table footer 1462 */ 1463 public function tabletfoot_close() { 1464 $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF; 1465 } 1466 1467 /** 1468 * Open a table row 1469 * 1470 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1471 */ 1472 public function tablerow_open($classes = null) { 1473 // initialize the cell counter used for classes 1474 $this->_counter['cell_counter'] = 0; 1475 $class = 'row'.$this->_counter['row_counter']++; 1476 if($classes !== null) { 1477 if(is_array($classes)) $classes = join(' ', $classes); 1478 $class .= ' ' . $classes; 1479 } 1480 $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB; 1481 } 1482 1483 /** 1484 * Close a table row 1485 */ 1486 public function tablerow_close() { 1487 $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF; 1488 } 1489 1490 /** 1491 * Open a table header cell 1492 * 1493 * @param int $colspan 1494 * @param string $align left|center|right 1495 * @param int $rowspan 1496 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1497 */ 1498 public function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) { 1499 $class = 'class="col'.$this->_counter['cell_counter']++; 1500 if(!is_null($align)) { 1501 $class .= ' '.$align.'align'; 1502 } 1503 if($classes !== null) { 1504 if(is_array($classes)) $classes = join(' ', $classes); 1505 $class .= ' ' . $classes; 1506 } 1507 $class .= '"'; 1508 $this->doc .= '<th '.$class; 1509 if($colspan > 1) { 1510 $this->_counter['cell_counter'] += $colspan - 1; 1511 $this->doc .= ' colspan="'.$colspan.'"'; 1512 } 1513 if($rowspan > 1) { 1514 $this->doc .= ' rowspan="'.$rowspan.'"'; 1515 } 1516 $this->doc .= '>'; 1517 } 1518 1519 /** 1520 * Close a table header cell 1521 */ 1522 public function tableheader_close() { 1523 $this->doc .= '</th>'; 1524 } 1525 1526 /** 1527 * Open a table cell 1528 * 1529 * @param int $colspan 1530 * @param string $align left|center|right 1531 * @param int $rowspan 1532 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1533 */ 1534 public function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) { 1535 $class = 'class="col'.$this->_counter['cell_counter']++; 1536 if(!is_null($align)) { 1537 $class .= ' '.$align.'align'; 1538 } 1539 if($classes !== null) { 1540 if(is_array($classes)) $classes = join(' ', $classes); 1541 $class .= ' ' . $classes; 1542 } 1543 $class .= '"'; 1544 $this->doc .= '<td '.$class; 1545 if($colspan > 1) { 1546 $this->_counter['cell_counter'] += $colspan - 1; 1547 $this->doc .= ' colspan="'.$colspan.'"'; 1548 } 1549 if($rowspan > 1) { 1550 $this->doc .= ' rowspan="'.$rowspan.'"'; 1551 } 1552 $this->doc .= '>'; 1553 } 1554 1555 /** 1556 * Close a table cell 1557 */ 1558 public function tablecell_close() { 1559 $this->doc .= '</td>'; 1560 } 1561 1562 /** 1563 * Returns the current header level. 1564 * (required e.g. by the filelist plugin) 1565 * 1566 * @return int The current header level 1567 */ 1568 public function getLastlevel() { 1569 return $this->lastlevel; 1570 } 1571 1572 #region Utility functions 1573 1574 /** 1575 * Build a link 1576 * 1577 * Assembles all parts defined in $link returns HTML for the link 1578 * 1579 * @param array $link attributes of a link 1580 * @return string 1581 * 1582 * @author Andreas Gohr <andi@splitbrain.org> 1583 */ 1584 public function _formatLink($link) { 1585 //make sure the url is XHTML compliant (skip mailto) 1586 if(substr($link['url'], 0, 7) != 'mailto:') { 1587 $link['url'] = str_replace('&', '&', $link['url']); 1588 $link['url'] = str_replace('&amp;', '&', $link['url']); 1589 } 1590 //remove double encodings in titles 1591 $link['title'] = str_replace('&amp;', '&', $link['title']); 1592 1593 // be sure there are no bad chars in url or title 1594 // (we can't do this for name because it can contain an img tag) 1595 $link['url'] = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22')); 1596 $link['title'] = strtr($link['title'], array('>' => '>', '<' => '<', '"' => '"')); 1597 1598 $ret = ''; 1599 $ret .= $link['pre']; 1600 $ret .= '<a href="'.$link['url'].'"'; 1601 if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"'; 1602 if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"'; 1603 if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"'; 1604 if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"'; 1605 if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"'; 1606 if(!empty($link['more'])) $ret .= ' '.$link['more']; 1607 $ret .= '>'; 1608 $ret .= $link['name']; 1609 $ret .= '</a>'; 1610 $ret .= $link['suf']; 1611 return $ret; 1612 } 1613 1614 /** 1615 * Renders internal and external media 1616 * 1617 * @author Andreas Gohr <andi@splitbrain.org> 1618 * @param string $src media ID 1619 * @param string $title descriptive text 1620 * @param string $align left|center|right 1621 * @param int $width width of media in pixel 1622 * @param int $height height of media in pixel 1623 * @param string $cache cache|recache|nocache 1624 * @param bool $render should the media be embedded inline or just linked 1625 * @return string 1626 */ 1627 public function _media($src, $title = null, $align = null, $width = null, 1628 $height = null, $cache = null, $render = true) { 1629 1630 $ret = ''; 1631 1632 list($ext, $mime) = mimetype($src); 1633 if(substr($mime, 0, 5) == 'image') { 1634 // first get the $title 1635 if(!is_null($title)) { 1636 $title = $this->_xmlEntities($title); 1637 } elseif($ext == 'jpg' || $ext == 'jpeg') { 1638 //try to use the caption from IPTC/EXIF 1639 require_once(DOKU_INC.'inc/JpegMeta.php'); 1640 $jpeg = new JpegMeta(mediaFN($src)); 1641 if($jpeg !== false) $cap = $jpeg->getTitle(); 1642 if(!empty($cap)) { 1643 $title = $this->_xmlEntities($cap); 1644 } 1645 } 1646 if(!$render) { 1647 // if the picture is not supposed to be rendered 1648 // return the title of the picture 1649 if($title === null || $title === "") { 1650 // just show the sourcename 1651 $title = $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src))); 1652 } 1653 return $title; 1654 } 1655 //add image tag 1656 $ret .= '<img src="' . ml( 1657 $src, 1658 array( 1659 'w' => $width, 'h' => $height, 1660 'cache' => $cache, 1661 'rev' => $this->_getLastMediaRevisionAt($src) 1662 ) 1663 ) . '"'; 1664 $ret .= ' class="media'.$align.'"'; 1665 $ret .= ' loading="lazy"'; 1666 1667 if($title) { 1668 $ret .= ' title="'.$title.'"'; 1669 $ret .= ' alt="'.$title.'"'; 1670 } else { 1671 $ret .= ' alt=""'; 1672 } 1673 1674 if(!is_null($width)) 1675 $ret .= ' width="'.$this->_xmlEntities($width).'"'; 1676 1677 if(!is_null($height)) 1678 $ret .= ' height="'.$this->_xmlEntities($height).'"'; 1679 1680 $ret .= ' />'; 1681 1682 } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) { 1683 // first get the $title 1684 $title = !is_null($title) ? $title : false; 1685 if(!$render) { 1686 // if the file is not supposed to be rendered 1687 // return the title of the file (just the sourcename if there is no title) 1688 return $this->_xmlEntities($title ? $title : \dokuwiki\Utf8\PhpString::basename(noNS($src))); 1689 } 1690 1691 $att = array(); 1692 $att['class'] = "media$align"; 1693 if($title) { 1694 $att['title'] = $title; 1695 } 1696 1697 if(media_supportedav($mime, 'video')) { 1698 //add video 1699 $ret .= $this->_video($src, $width, $height, $att); 1700 } 1701 if(media_supportedav($mime, 'audio')) { 1702 //add audio 1703 $ret .= $this->_audio($src, $att); 1704 } 1705 1706 } elseif($mime == 'application/x-shockwave-flash') { 1707 if(!$render) { 1708 // if the flash is not supposed to be rendered 1709 // return the title of the flash 1710 if(!$title) { 1711 // just show the sourcename 1712 $title = \dokuwiki\Utf8\PhpString::basename(noNS($src)); 1713 } 1714 return $this->_xmlEntities($title); 1715 } 1716 1717 $att = array(); 1718 $att['class'] = "media$align"; 1719 if($align == 'right') $att['align'] = 'right'; 1720 if($align == 'left') $att['align'] = 'left'; 1721 $ret .= html_flashobject( 1722 ml($src, array('cache' => $cache), true, '&'), $width, $height, 1723 array('quality' => 'high'), 1724 null, 1725 $att, 1726 $this->_xmlEntities($title) 1727 ); 1728 } elseif($title) { 1729 // well at least we have a title to display 1730 $ret .= $this->_xmlEntities($title); 1731 } else { 1732 // just show the sourcename 1733 $ret .= $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($src))); 1734 } 1735 1736 return $ret; 1737 } 1738 1739 /** 1740 * Escape string for output 1741 * 1742 * @param $string 1743 * @return string 1744 */ 1745 public function _xmlEntities($string) { 1746 return hsc($string); 1747 } 1748 1749 1750 1751 /** 1752 * Construct a title and handle images in titles 1753 * 1754 * @author Harry Fuecks <hfuecks@gmail.com> 1755 * @param string|array $title either string title or media array 1756 * @param string $default default title if nothing else is found 1757 * @param bool $isImage will be set to true if it's a media file 1758 * @param null|string $id linked page id (used to extract title from first heading) 1759 * @param string $linktype content|navigation 1760 * @return string HTML of the title, might be full image tag or just escaped text 1761 */ 1762 public function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') { 1763 $isImage = false; 1764 if(is_array($title)) { 1765 $isImage = true; 1766 return $this->_imageTitle($title); 1767 } elseif(is_null($title) || trim($title) == '') { 1768 if(useHeading($linktype) && $id) { 1769 $heading = p_get_first_heading($id); 1770 if(!blank($heading)) { 1771 return $this->_xmlEntities($heading); 1772 } 1773 } 1774 return $this->_xmlEntities($default); 1775 } else { 1776 return $this->_xmlEntities($title); 1777 } 1778 } 1779 1780 /** 1781 * Returns HTML code for images used in link titles 1782 * 1783 * @author Andreas Gohr <andi@splitbrain.org> 1784 * @param array $img 1785 * @return string HTML img tag or similar 1786 */ 1787 public function _imageTitle($img) { 1788 global $ID; 1789 1790 // some fixes on $img['src'] 1791 // see internalmedia() and externalmedia() 1792 list($img['src']) = explode('#', $img['src'], 2); 1793 if($img['type'] == 'internalmedia') { 1794 $img['src'] = (new MediaResolver($ID))->resolveId($img['src'], $this->date_at, true); 1795 } 1796 1797 return $this->_media( 1798 $img['src'], 1799 $img['title'], 1800 $img['align'], 1801 $img['width'], 1802 $img['height'], 1803 $img['cache'] 1804 ); 1805 } 1806 1807 /** 1808 * helperfunction to return a basic link to a media 1809 * 1810 * used in internalmedia() and externalmedia() 1811 * 1812 * @author Pierre Spring <pierre.spring@liip.ch> 1813 * @param string $src media ID 1814 * @param string $title descriptive text 1815 * @param string $align left|center|right 1816 * @param int $width width of media in pixel 1817 * @param int $height height of media in pixel 1818 * @param string $cache cache|recache|nocache 1819 * @param bool $render should the media be embedded inline or just linked 1820 * @return array associative array with link config 1821 */ 1822 public function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) { 1823 global $conf; 1824 1825 $link = array(); 1826 $link['class'] = 'media'; 1827 $link['style'] = ''; 1828 $link['pre'] = ''; 1829 $link['suf'] = ''; 1830 $link['more'] = ''; 1831 $link['target'] = $conf['target']['media']; 1832 if($conf['target']['media']) $link['rel'] = 'noopener'; 1833 $link['title'] = $this->_xmlEntities($src); 1834 $link['name'] = $this->_media($src, $title, $align, $width, $height, $cache, $render); 1835 1836 return $link; 1837 } 1838 1839 /** 1840 * Embed video(s) in HTML 1841 * 1842 * @author Anika Henke <anika@selfthinker.org> 1843 * @author Schplurtz le Déboulonné <Schplurtz@laposte.net> 1844 * 1845 * @param string $src - ID of video to embed 1846 * @param int $width - width of the video in pixels 1847 * @param int $height - height of the video in pixels 1848 * @param array $atts - additional attributes for the <video> tag 1849 * @return string 1850 */ 1851 public function _video($src, $width, $height, $atts = null) { 1852 // prepare width and height 1853 if(is_null($atts)) $atts = array(); 1854 $atts['width'] = (int) $width; 1855 $atts['height'] = (int) $height; 1856 if(!$atts['width']) $atts['width'] = 320; 1857 if(!$atts['height']) $atts['height'] = 240; 1858 1859 $posterUrl = ''; 1860 $files = array(); 1861 $tracks = array(); 1862 $isExternal = media_isexternal($src); 1863 1864 if ($isExternal) { 1865 // take direct source for external files 1866 list(/*ext*/, $srcMime) = mimetype($src); 1867 $files[$srcMime] = $src; 1868 } else { 1869 // prepare alternative formats 1870 $extensions = array('webm', 'ogv', 'mp4'); 1871 $files = media_alternativefiles($src, $extensions); 1872 $poster = media_alternativefiles($src, array('jpg', 'png')); 1873 $tracks = media_trackfiles($src); 1874 if(!empty($poster)) { 1875 $posterUrl = ml(reset($poster), '', true, '&'); 1876 } 1877 } 1878 1879 $out = ''; 1880 // open video tag 1881 $out .= '<video '.buildAttributes($atts).' controls="controls"'; 1882 if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"'; 1883 $out .= '>'.NL; 1884 $fallback = ''; 1885 1886 // output source for each alternative video format 1887 foreach($files as $mime => $file) { 1888 if ($isExternal) { 1889 $url = $file; 1890 $linkType = 'externalmedia'; 1891 } else { 1892 $url = ml($file, '', true, '&'); 1893 $linkType = 'internalmedia'; 1894 } 1895 $title = !empty($atts['title']) 1896 ? $atts['title'] 1897 : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file))); 1898 1899 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1900 // alternative content (just a link to the file) 1901 $fallback .= $this->$linkType( 1902 $file, 1903 $title, 1904 null, 1905 null, 1906 null, 1907 $cache = null, 1908 $linking = 'linkonly', 1909 $return = true 1910 ); 1911 } 1912 1913 // output each track if any 1914 foreach( $tracks as $trackid => $info ) { 1915 list( $kind, $srclang ) = array_map( 'hsc', $info ); 1916 $out .= "<track kind=\"$kind\" srclang=\"$srclang\" "; 1917 $out .= "label=\"$srclang\" "; 1918 $out .= 'src="'.ml($trackid, '', true).'">'.NL; 1919 } 1920 1921 // finish 1922 $out .= $fallback; 1923 $out .= '</video>'.NL; 1924 return $out; 1925 } 1926 1927 /** 1928 * Embed audio in HTML 1929 * 1930 * @author Anika Henke <anika@selfthinker.org> 1931 * 1932 * @param string $src - ID of audio to embed 1933 * @param array $atts - additional attributes for the <audio> tag 1934 * @return string 1935 */ 1936 public function _audio($src, $atts = array()) { 1937 $files = array(); 1938 $isExternal = media_isexternal($src); 1939 1940 if ($isExternal) { 1941 // take direct source for external files 1942 list(/*ext*/, $srcMime) = mimetype($src); 1943 $files[$srcMime] = $src; 1944 } else { 1945 // prepare alternative formats 1946 $extensions = array('ogg', 'mp3', 'wav'); 1947 $files = media_alternativefiles($src, $extensions); 1948 } 1949 1950 $out = ''; 1951 // open audio tag 1952 $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL; 1953 $fallback = ''; 1954 1955 // output source for each alternative audio format 1956 foreach($files as $mime => $file) { 1957 if ($isExternal) { 1958 $url = $file; 1959 $linkType = 'externalmedia'; 1960 } else { 1961 $url = ml($file, '', true, '&'); 1962 $linkType = 'internalmedia'; 1963 } 1964 $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(\dokuwiki\Utf8\PhpString::basename(noNS($file))); 1965 1966 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1967 // alternative content (just a link to the file) 1968 $fallback .= $this->$linkType( 1969 $file, 1970 $title, 1971 null, 1972 null, 1973 null, 1974 $cache = null, 1975 $linking = 'linkonly', 1976 $return = true 1977 ); 1978 } 1979 1980 // finish 1981 $out .= $fallback; 1982 $out .= '</audio>'.NL; 1983 return $out; 1984 } 1985 1986 /** 1987 * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media() 1988 * which returns an existing media revision less or equal to rev or date_at 1989 * 1990 * @author lisps 1991 * @param string $media_id 1992 * @access protected 1993 * @return string revision ('' for current) 1994 */ 1995 protected function _getLastMediaRevisionAt($media_id) { 1996 if (!$this->date_at || media_isexternal($media_id)) return ''; 1997 $changelog = new MediaChangeLog($media_id); 1998 return $changelog->getLastRevisionAt($this->date_at); 1999 } 2000 2001 #endregion 2002} 2003 2004//Setup VIM: ex: et ts=4 : 2005