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