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