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