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