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