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