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