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