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