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