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