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