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