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