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