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'.hsc(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 * @param array $options assoziative array with additional geshi options 637 */ 638 function file($text, $language = null, $filename = null, $options=null) { 639 $this->_highlight('file', $text, $language, $filename, $options); 640 } 641 642 /** 643 * Display text as code content, optionally syntax highlighted 644 * 645 * @param string $text text to show 646 * @param string $language programming language to use for syntax highlighting 647 * @param string $filename file path label 648 * @param array $options assoziative array with additional geshi options 649 */ 650 function code($text, $language = null, $filename = null, $options=null) { 651 $this->_highlight('code', $text, $language, $filename, $options); 652 } 653 654 /** 655 * Use GeSHi to highlight language syntax in code and file blocks 656 * 657 * @author Andreas Gohr <andi@splitbrain.org> 658 * @param string $type code|file 659 * @param string $text text to show 660 * @param string $language programming language to use for syntax highlighting 661 * @param string $filename file path label 662 * @param array $options assoziative array with additional geshi options 663 */ 664 function _highlight($type, $text, $language = null, $filename = null, $options = null) { 665 global $ID; 666 global $lang; 667 global $INPUT; 668 669 $language = preg_replace(PREG_PATTERN_VALID_LANGUAGE, '', $language); 670 671 if($filename) { 672 // add icon 673 list($ext) = mimetype($filename, false); 674 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 675 $class = 'mediafile mf_'.$class; 676 677 $offset = 0; 678 if ($INPUT->has('codeblockOffset')) { 679 $offset = $INPUT->str('codeblockOffset'); 680 } 681 $this->doc .= '<dl class="'.$type.'">'.DOKU_LF; 682 $this->doc .= '<dt><a href="'.exportlink($ID, 'code', array('codeblock' => $offset+$this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">'; 683 $this->doc .= hsc($filename); 684 $this->doc .= '</a></dt>'.DOKU_LF.'<dd>'; 685 } 686 687 if($text{0} == "\n") { 688 $text = substr($text, 1); 689 } 690 if(substr($text, -1) == "\n") { 691 $text = substr($text, 0, -1); 692 } 693 694 if(empty($language)) { // empty is faster than is_null and can prevent '' string 695 $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF; 696 } else { 697 $class = 'code'; //we always need the code class to make the syntax highlighting apply 698 if($type != 'code') $class .= ' '.$type; 699 700 $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '', $options).'</pre>'.DOKU_LF; 701 } 702 703 if($filename) { 704 $this->doc .= '</dd></dl>'.DOKU_LF; 705 } 706 707 $this->_codeblock++; 708 } 709 710 /** 711 * Format an acronym 712 * 713 * Uses $this->acronyms 714 * 715 * @param string $acronym 716 */ 717 function acronym($acronym) { 718 719 if(array_key_exists($acronym, $this->acronyms)) { 720 721 $title = $this->_xmlEntities($this->acronyms[$acronym]); 722 723 $this->doc .= '<abbr title="'.$title 724 .'">'.$this->_xmlEntities($acronym).'</abbr>'; 725 726 } else { 727 $this->doc .= $this->_xmlEntities($acronym); 728 } 729 } 730 731 /** 732 * Format a smiley 733 * 734 * Uses $this->smiley 735 * 736 * @param string $smiley 737 */ 738 function smiley($smiley) { 739 if(array_key_exists($smiley, $this->smileys)) { 740 $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley]. 741 '" class="icon" alt="'. 742 $this->_xmlEntities($smiley).'" />'; 743 } else { 744 $this->doc .= $this->_xmlEntities($smiley); 745 } 746 } 747 748 /** 749 * Format an entity 750 * 751 * Entities are basically small text replacements 752 * 753 * Uses $this->entities 754 * 755 * @param string $entity 756 */ 757 function entity($entity) { 758 if(array_key_exists($entity, $this->entities)) { 759 $this->doc .= $this->entities[$entity]; 760 } else { 761 $this->doc .= $this->_xmlEntities($entity); 762 } 763 } 764 765 /** 766 * Typographically format a multiply sign 767 * 768 * Example: ($x=640, $y=480) should result in "640×480" 769 * 770 * @param string|int $x first value 771 * @param string|int $y second value 772 */ 773 function multiplyentity($x, $y) { 774 $this->doc .= "$x×$y"; 775 } 776 777 /** 778 * Render an opening single quote char (language specific) 779 */ 780 function singlequoteopening() { 781 global $lang; 782 $this->doc .= $lang['singlequoteopening']; 783 } 784 785 /** 786 * Render a closing single quote char (language specific) 787 */ 788 function singlequoteclosing() { 789 global $lang; 790 $this->doc .= $lang['singlequoteclosing']; 791 } 792 793 /** 794 * Render an apostrophe char (language specific) 795 */ 796 function apostrophe() { 797 global $lang; 798 $this->doc .= $lang['apostrophe']; 799 } 800 801 /** 802 * Render an opening double quote char (language specific) 803 */ 804 function doublequoteopening() { 805 global $lang; 806 $this->doc .= $lang['doublequoteopening']; 807 } 808 809 /** 810 * Render an closinging double quote char (language specific) 811 */ 812 function doublequoteclosing() { 813 global $lang; 814 $this->doc .= $lang['doublequoteclosing']; 815 } 816 817 /** 818 * Render a CamelCase link 819 * 820 * @param string $link The link name 821 * @param bool $returnonly whether to return html or write to doc attribute 822 * @return void|string writes to doc attribute or returns html depends on $returnonly 823 * 824 * @see http://en.wikipedia.org/wiki/CamelCase 825 */ 826 function camelcaselink($link, $returnonly = false) { 827 if($returnonly) { 828 return $this->internallink($link, $link, null, true); 829 } else { 830 $this->internallink($link, $link); 831 } 832 } 833 834 /** 835 * Render a page local link 836 * 837 * @param string $hash hash link identifier 838 * @param string $name name for the link 839 * @param bool $returnonly whether to return html or write to doc attribute 840 * @return void|string writes to doc attribute or returns html depends on $returnonly 841 */ 842 function locallink($hash, $name = null, $returnonly = false) { 843 global $ID; 844 $name = $this->_getLinkTitle($name, $hash, $isImage); 845 $hash = $this->_headerToLink($hash); 846 $title = $ID.' ↵'; 847 848 $doc = '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">'; 849 $doc .= $name; 850 $doc .= '</a>'; 851 852 if($returnonly) { 853 return $doc; 854 } else { 855 $this->doc .= $doc; 856 } 857 } 858 859 /** 860 * Render an internal Wiki Link 861 * 862 * $search,$returnonly & $linktype are not for the renderer but are used 863 * elsewhere - no need to implement them in other renderers 864 * 865 * @author Andreas Gohr <andi@splitbrain.org> 866 * @param string $id pageid 867 * @param string|null $name link name 868 * @param string|null $search adds search url param 869 * @param bool $returnonly whether to return html or write to doc attribute 870 * @param string $linktype type to set use of headings 871 * @return void|string writes to doc attribute or returns html depends on $returnonly 872 */ 873 function internallink($id, $name = null, $search = null, $returnonly = false, $linktype = 'content') { 874 global $conf; 875 global $ID; 876 global $INFO; 877 878 $params = ''; 879 $parts = explode('?', $id, 2); 880 if(count($parts) === 2) { 881 $id = $parts[0]; 882 $params = $parts[1]; 883 } 884 885 // For empty $id we need to know the current $ID 886 // We need this check because _simpleTitle needs 887 // correct $id and resolve_pageid() use cleanID($id) 888 // (some things could be lost) 889 if($id === '') { 890 $id = $ID; 891 } 892 893 // default name is based on $id as given 894 $default = $this->_simpleTitle($id); 895 896 // now first resolve and clean up the $id 897 resolve_pageid(getNS($ID), $id, $exists, $this->date_at, true); 898 899 $link = array(); 900 $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype); 901 if(!$isImage) { 902 if($exists) { 903 $class = 'wikilink1'; 904 } else { 905 $class = 'wikilink2'; 906 $link['rel'] = 'nofollow'; 907 } 908 } else { 909 $class = 'media'; 910 } 911 912 //keep hash anchor 913 @list($id, $hash) = explode('#', $id, 2); 914 if(!empty($hash)) $hash = $this->_headerToLink($hash); 915 916 //prepare for formating 917 $link['target'] = $conf['target']['wiki']; 918 $link['style'] = ''; 919 $link['pre'] = ''; 920 $link['suf'] = ''; 921 // highlight link to current page 922 if($id == $INFO['id']) { 923 $link['pre'] = '<span class="curid">'; 924 $link['suf'] = '</span>'; 925 } 926 $link['more'] = ''; 927 $link['class'] = $class; 928 if($this->date_at) { 929 $params = $params.'&at='.rawurlencode($this->date_at); 930 } 931 $link['url'] = wl($id, $params); 932 $link['name'] = $name; 933 $link['title'] = $id; 934 //add search string 935 if($search) { 936 ($conf['userewrite']) ? $link['url'] .= '?' : $link['url'] .= '&'; 937 if(is_array($search)) { 938 $search = array_map('rawurlencode', $search); 939 $link['url'] .= 's[]='.join('&s[]=', $search); 940 } else { 941 $link['url'] .= 's='.rawurlencode($search); 942 } 943 } 944 945 //keep hash 946 if($hash) $link['url'] .= '#'.$hash; 947 948 //output formatted 949 if($returnonly) { 950 return $this->_formatLink($link); 951 } else { 952 $this->doc .= $this->_formatLink($link); 953 } 954 } 955 956 /** 957 * Render an external link 958 * 959 * @param string $url full URL with scheme 960 * @param string|array $name name for the link, array for media file 961 * @param bool $returnonly whether to return html or write to doc attribute 962 * @return void|string writes to doc attribute or returns html depends on $returnonly 963 */ 964 function externallink($url, $name = null, $returnonly = false) { 965 global $conf; 966 967 $name = $this->_getLinkTitle($name, $url, $isImage); 968 969 // url might be an attack vector, only allow registered protocols 970 if(is_null($this->schemes)) $this->schemes = getSchemes(); 971 list($scheme) = explode('://', $url); 972 $scheme = strtolower($scheme); 973 if(!in_array($scheme, $this->schemes)) $url = ''; 974 975 // is there still an URL? 976 if(!$url) { 977 if($returnonly) { 978 return $name; 979 } else { 980 $this->doc .= $name; 981 } 982 return; 983 } 984 985 // set class 986 if(!$isImage) { 987 $class = 'urlextern'; 988 } else { 989 $class = 'media'; 990 } 991 992 //prepare for formating 993 $link = array(); 994 $link['target'] = $conf['target']['extern']; 995 $link['style'] = ''; 996 $link['pre'] = ''; 997 $link['suf'] = ''; 998 $link['more'] = ''; 999 $link['class'] = $class; 1000 $link['url'] = $url; 1001 $link['rel'] = ''; 1002 1003 $link['name'] = $name; 1004 $link['title'] = $this->_xmlEntities($url); 1005 if($conf['relnofollow']) $link['rel'] .= ' nofollow'; 1006 if($conf['target']['extern']) $link['rel'] .= ' noopener'; 1007 1008 //output formatted 1009 if($returnonly) { 1010 return $this->_formatLink($link); 1011 } else { 1012 $this->doc .= $this->_formatLink($link); 1013 } 1014 } 1015 1016 /** 1017 * Render an interwiki link 1018 * 1019 * You may want to use $this->_resolveInterWiki() here 1020 * 1021 * @param string $match original link - probably not much use 1022 * @param string|array $name name for the link, array for media file 1023 * @param string $wikiName indentifier (shortcut) for the remote wiki 1024 * @param string $wikiUri the fragment parsed from the original link 1025 * @param bool $returnonly whether to return html or write to doc attribute 1026 * @return void|string writes to doc attribute or returns html depends on $returnonly 1027 */ 1028 function interwikilink($match, $name = null, $wikiName, $wikiUri, $returnonly = false) { 1029 global $conf; 1030 1031 $link = array(); 1032 $link['target'] = $conf['target']['interwiki']; 1033 $link['pre'] = ''; 1034 $link['suf'] = ''; 1035 $link['more'] = ''; 1036 $link['name'] = $this->_getLinkTitle($name, $wikiUri, $isImage); 1037 $link['rel'] = ''; 1038 1039 //get interwiki URL 1040 $exists = null; 1041 $url = $this->_resolveInterWiki($wikiName, $wikiUri, $exists); 1042 1043 if(!$isImage) { 1044 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName); 1045 $link['class'] = "interwiki iw_$class"; 1046 } else { 1047 $link['class'] = 'media'; 1048 } 1049 1050 //do we stay at the same server? Use local target 1051 if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) { 1052 $link['target'] = $conf['target']['wiki']; 1053 } 1054 if($exists !== null && !$isImage) { 1055 if($exists) { 1056 $link['class'] .= ' wikilink1'; 1057 } else { 1058 $link['class'] .= ' wikilink2'; 1059 $link['rel'] .= ' nofollow'; 1060 } 1061 } 1062 if($conf['target']['interwiki']) $link['rel'] .= ' noopener'; 1063 1064 $link['url'] = $url; 1065 $link['title'] = htmlspecialchars($link['url']); 1066 1067 //output formatted 1068 if($returnonly) { 1069 return $this->_formatLink($link); 1070 } else { 1071 $this->doc .= $this->_formatLink($link); 1072 } 1073 } 1074 1075 /** 1076 * Link to windows share 1077 * 1078 * @param string $url the link 1079 * @param string|array $name name for the link, array for media file 1080 * @param bool $returnonly whether to return html or write to doc attribute 1081 * @return void|string writes to doc attribute or returns html depends on $returnonly 1082 */ 1083 function windowssharelink($url, $name = null, $returnonly = false) { 1084 global $conf; 1085 1086 //simple setup 1087 $link = array(); 1088 $link['target'] = $conf['target']['windows']; 1089 $link['pre'] = ''; 1090 $link['suf'] = ''; 1091 $link['style'] = ''; 1092 1093 $link['name'] = $this->_getLinkTitle($name, $url, $isImage); 1094 if(!$isImage) { 1095 $link['class'] = 'windows'; 1096 } else { 1097 $link['class'] = 'media'; 1098 } 1099 1100 $link['title'] = $this->_xmlEntities($url); 1101 $url = str_replace('\\', '/', $url); 1102 $url = 'file:///'.$url; 1103 $link['url'] = $url; 1104 1105 //output formatted 1106 if($returnonly) { 1107 return $this->_formatLink($link); 1108 } else { 1109 $this->doc .= $this->_formatLink($link); 1110 } 1111 } 1112 1113 /** 1114 * Render a linked E-Mail Address 1115 * 1116 * Honors $conf['mailguard'] setting 1117 * 1118 * @param string $address Email-Address 1119 * @param string|array $name name for the link, array for media file 1120 * @param bool $returnonly whether to return html or write to doc attribute 1121 * @return void|string writes to doc attribute or returns html depends on $returnonly 1122 */ 1123 function emaillink($address, $name = null, $returnonly = false) { 1124 global $conf; 1125 //simple setup 1126 $link = array(); 1127 $link['target'] = ''; 1128 $link['pre'] = ''; 1129 $link['suf'] = ''; 1130 $link['style'] = ''; 1131 $link['more'] = ''; 1132 1133 $name = $this->_getLinkTitle($name, '', $isImage); 1134 if(!$isImage) { 1135 $link['class'] = 'mail'; 1136 } else { 1137 $link['class'] = 'media'; 1138 } 1139 1140 $address = $this->_xmlEntities($address); 1141 $address = obfuscate($address); 1142 $title = $address; 1143 1144 if(empty($name)) { 1145 $name = $address; 1146 } 1147 1148 if($conf['mailguard'] == 'visible') $address = rawurlencode($address); 1149 1150 $link['url'] = 'mailto:'.$address; 1151 $link['name'] = $name; 1152 $link['title'] = $title; 1153 1154 //output formatted 1155 if($returnonly) { 1156 return $this->_formatLink($link); 1157 } else { 1158 $this->doc .= $this->_formatLink($link); 1159 } 1160 } 1161 1162 /** 1163 * Render an internal media file 1164 * 1165 * @param string $src media ID 1166 * @param string $title descriptive text 1167 * @param string $align left|center|right 1168 * @param int $width width of media in pixel 1169 * @param int $height height of media in pixel 1170 * @param string $cache cache|recache|nocache 1171 * @param string $linking linkonly|detail|nolink 1172 * @param bool $return return HTML instead of adding to $doc 1173 * @return void|string writes to doc attribute or returns html depends on $return 1174 */ 1175 function internalmedia($src, $title = null, $align = null, $width = null, 1176 $height = null, $cache = null, $linking = null, $return = false) { 1177 global $ID; 1178 if (strpos($src, '#') !== false) { 1179 list($src, $hash) = explode('#', $src, 2); 1180 } 1181 resolve_mediaid(getNS($ID), $src, $exists, $this->date_at, true); 1182 1183 $noLink = false; 1184 $render = ($linking == 'linkonly') ? false : true; 1185 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 1186 1187 list($ext, $mime) = mimetype($src, false); 1188 if(substr($mime, 0, 5) == 'image' && $render) { 1189 $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src)), ($linking == 'direct')); 1190 } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) { 1191 // don't link movies 1192 $noLink = true; 1193 } else { 1194 // add file icons 1195 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 1196 $link['class'] .= ' mediafile mf_'.$class; 1197 $link['url'] = ml($src, array('id' => $ID, 'cache' => $cache , 'rev'=>$this->_getLastMediaRevisionAt($src)), true); 1198 if($exists) $link['title'] .= ' ('.filesize_h(filesize(mediaFN($src))).')'; 1199 } 1200 1201 if (!empty($hash)) $link['url'] .= '#'.$hash; 1202 1203 //markup non existing files 1204 if(!$exists) { 1205 $link['class'] .= ' wikilink2'; 1206 } 1207 1208 //output formatted 1209 if($return) { 1210 if($linking == 'nolink' || $noLink) return $link['name']; 1211 else return $this->_formatLink($link); 1212 } else { 1213 if($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 1214 else $this->doc .= $this->_formatLink($link); 1215 } 1216 } 1217 1218 /** 1219 * Render an external media file 1220 * 1221 * @param string $src full media URL 1222 * @param string $title descriptive text 1223 * @param string $align left|center|right 1224 * @param int $width width of media in pixel 1225 * @param int $height height of media in pixel 1226 * @param string $cache cache|recache|nocache 1227 * @param string $linking linkonly|detail|nolink 1228 * @param bool $return return HTML instead of adding to $doc 1229 * @return void|string writes to doc attribute or returns html depends on $return 1230 */ 1231 function externalmedia($src, $title = null, $align = null, $width = null, 1232 $height = null, $cache = null, $linking = null, $return = false) { 1233 if(link_isinterwiki($src)){ 1234 list($shortcut, $reference) = explode('>', $src, 2); 1235 $exists = null; 1236 $src = $this->_resolveInterWiki($shortcut, $reference, $exists); 1237 } 1238 list($src, $hash) = explode('#', $src, 2); 1239 $noLink = false; 1240 $render = ($linking == 'linkonly') ? false : true; 1241 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 1242 1243 $link['url'] = ml($src, array('cache' => $cache)); 1244 1245 list($ext, $mime) = mimetype($src, false); 1246 if(substr($mime, 0, 5) == 'image' && $render) { 1247 // link only jpeg images 1248 // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true; 1249 } elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render) { 1250 // don't link movies 1251 $noLink = true; 1252 } else { 1253 // add file icons 1254 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $ext); 1255 $link['class'] .= ' mediafile mf_'.$class; 1256 } 1257 1258 if($hash) $link['url'] .= '#'.$hash; 1259 1260 //output formatted 1261 if($return) { 1262 if($linking == 'nolink' || $noLink) return $link['name']; 1263 else return $this->_formatLink($link); 1264 } else { 1265 if($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 1266 else $this->doc .= $this->_formatLink($link); 1267 } 1268 } 1269 1270 /** 1271 * Renders an RSS feed 1272 * 1273 * @param string $url URL of the feed 1274 * @param array $params Finetuning of the output 1275 * 1276 * @author Andreas Gohr <andi@splitbrain.org> 1277 */ 1278 function rss($url, $params) { 1279 global $lang; 1280 global $conf; 1281 1282 require_once(DOKU_INC.'inc/FeedParser.php'); 1283 $feed = new FeedParser(); 1284 $feed->set_feed_url($url); 1285 1286 //disable warning while fetching 1287 if(!defined('DOKU_E_LEVEL')) { 1288 $elvl = error_reporting(E_ERROR); 1289 } 1290 $rc = $feed->init(); 1291 if(isset($elvl)) { 1292 error_reporting($elvl); 1293 } 1294 1295 if($params['nosort']) $feed->enable_order_by_date(false); 1296 1297 //decide on start and end 1298 if($params['reverse']) { 1299 $mod = -1; 1300 $start = $feed->get_item_quantity() - 1; 1301 $end = $start - ($params['max']); 1302 $end = ($end < -1) ? -1 : $end; 1303 } else { 1304 $mod = 1; 1305 $start = 0; 1306 $end = $feed->get_item_quantity(); 1307 $end = ($end > $params['max']) ? $params['max'] : $end; 1308 } 1309 1310 $this->doc .= '<ul class="rss">'; 1311 if($rc) { 1312 for($x = $start; $x != $end; $x += $mod) { 1313 $item = $feed->get_item($x); 1314 $this->doc .= '<li><div class="li">'; 1315 // support feeds without links 1316 $lnkurl = $item->get_permalink(); 1317 if($lnkurl) { 1318 // title is escaped by SimplePie, we unescape here because it 1319 // is escaped again in externallink() FS#1705 1320 $this->externallink( 1321 $item->get_permalink(), 1322 html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8') 1323 ); 1324 } else { 1325 $this->doc .= ' '.$item->get_title(); 1326 } 1327 if($params['author']) { 1328 $author = $item->get_author(0); 1329 if($author) { 1330 $name = $author->get_name(); 1331 if(!$name) $name = $author->get_email(); 1332 if($name) $this->doc .= ' '.$lang['by'].' '.hsc($name); 1333 } 1334 } 1335 if($params['date']) { 1336 $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')'; 1337 } 1338 if($params['details']) { 1339 $this->doc .= '<div class="detail">'; 1340 if($conf['htmlok']) { 1341 $this->doc .= $item->get_description(); 1342 } else { 1343 $this->doc .= strip_tags($item->get_description()); 1344 } 1345 $this->doc .= '</div>'; 1346 } 1347 1348 $this->doc .= '</div></li>'; 1349 } 1350 } else { 1351 $this->doc .= '<li><div class="li">'; 1352 $this->doc .= '<em>'.$lang['rssfailed'].'</em>'; 1353 $this->externallink($url); 1354 if($conf['allowdebug']) { 1355 $this->doc .= '<!--'.hsc($feed->error).'-->'; 1356 } 1357 $this->doc .= '</div></li>'; 1358 } 1359 $this->doc .= '</ul>'; 1360 } 1361 1362 /** 1363 * Start a table 1364 * 1365 * @param int $maxcols maximum number of columns 1366 * @param int $numrows NOT IMPLEMENTED 1367 * @param int $pos byte position in the original source 1368 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1369 */ 1370 function table_open($maxcols = null, $numrows = null, $pos = null, $classes = null) { 1371 // initialize the row counter used for classes 1372 $this->_counter['row_counter'] = 0; 1373 $class = 'table'; 1374 if($classes !== null) { 1375 if(is_array($classes)) $classes = join(' ', $classes); 1376 $class .= ' ' . $classes; 1377 } 1378 if($pos !== null) { 1379 $hid = $this->_headerToLink($class, true); 1380 $data = array(); 1381 $data['target'] = 'table'; 1382 $data['name'] = ''; 1383 $data['hid'] = $hid; 1384 $class .= ' '.$this->startSectionEdit($pos, $data); 1385 } 1386 $this->doc .= '<div class="'.$class.'"><table class="inline">'. 1387 DOKU_LF; 1388 } 1389 1390 /** 1391 * Close a table 1392 * 1393 * @param int $pos byte position in the original source 1394 */ 1395 function table_close($pos = null) { 1396 $this->doc .= '</table></div>'.DOKU_LF; 1397 if($pos !== null) { 1398 $this->finishSectionEdit($pos); 1399 } 1400 } 1401 1402 /** 1403 * Open a table header 1404 */ 1405 function tablethead_open() { 1406 $this->doc .= DOKU_TAB.'<thead>'.DOKU_LF; 1407 } 1408 1409 /** 1410 * Close a table header 1411 */ 1412 function tablethead_close() { 1413 $this->doc .= DOKU_TAB.'</thead>'.DOKU_LF; 1414 } 1415 1416 /** 1417 * Open a table body 1418 */ 1419 function tabletbody_open() { 1420 $this->doc .= DOKU_TAB.'<tbody>'.DOKU_LF; 1421 } 1422 1423 /** 1424 * Close a table body 1425 */ 1426 function tabletbody_close() { 1427 $this->doc .= DOKU_TAB.'</tbody>'.DOKU_LF; 1428 } 1429 1430 /** 1431 * Open a table footer 1432 */ 1433 function tabletfoot_open() { 1434 $this->doc .= DOKU_TAB.'<tfoot>'.DOKU_LF; 1435 } 1436 1437 /** 1438 * Close a table footer 1439 */ 1440 function tabletfoot_close() { 1441 $this->doc .= DOKU_TAB.'</tfoot>'.DOKU_LF; 1442 } 1443 1444 /** 1445 * Open a table row 1446 * 1447 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1448 */ 1449 function tablerow_open($classes = null) { 1450 // initialize the cell counter used for classes 1451 $this->_counter['cell_counter'] = 0; 1452 $class = 'row'.$this->_counter['row_counter']++; 1453 if($classes !== null) { 1454 if(is_array($classes)) $classes = join(' ', $classes); 1455 $class .= ' ' . $classes; 1456 } 1457 $this->doc .= DOKU_TAB.'<tr class="'.$class.'">'.DOKU_LF.DOKU_TAB.DOKU_TAB; 1458 } 1459 1460 /** 1461 * Close a table row 1462 */ 1463 function tablerow_close() { 1464 $this->doc .= DOKU_LF.DOKU_TAB.'</tr>'.DOKU_LF; 1465 } 1466 1467 /** 1468 * Open a table header cell 1469 * 1470 * @param int $colspan 1471 * @param string $align left|center|right 1472 * @param int $rowspan 1473 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1474 */ 1475 function tableheader_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) { 1476 $class = 'class="col'.$this->_counter['cell_counter']++; 1477 if(!is_null($align)) { 1478 $class .= ' '.$align.'align'; 1479 } 1480 if($classes !== null) { 1481 if(is_array($classes)) $classes = join(' ', $classes); 1482 $class .= ' ' . $classes; 1483 } 1484 $class .= '"'; 1485 $this->doc .= '<th '.$class; 1486 if($colspan > 1) { 1487 $this->_counter['cell_counter'] += $colspan - 1; 1488 $this->doc .= ' colspan="'.$colspan.'"'; 1489 } 1490 if($rowspan > 1) { 1491 $this->doc .= ' rowspan="'.$rowspan.'"'; 1492 } 1493 $this->doc .= '>'; 1494 } 1495 1496 /** 1497 * Close a table header cell 1498 */ 1499 function tableheader_close() { 1500 $this->doc .= '</th>'; 1501 } 1502 1503 /** 1504 * Open a table cell 1505 * 1506 * @param int $colspan 1507 * @param string $align left|center|right 1508 * @param int $rowspan 1509 * @param string|string[] $classes css classes - have to be valid, do not pass unfiltered user input 1510 */ 1511 function tablecell_open($colspan = 1, $align = null, $rowspan = 1, $classes = null) { 1512 $class = 'class="col'.$this->_counter['cell_counter']++; 1513 if(!is_null($align)) { 1514 $class .= ' '.$align.'align'; 1515 } 1516 if($classes !== null) { 1517 if(is_array($classes)) $classes = join(' ', $classes); 1518 $class .= ' ' . $classes; 1519 } 1520 $class .= '"'; 1521 $this->doc .= '<td '.$class; 1522 if($colspan > 1) { 1523 $this->_counter['cell_counter'] += $colspan - 1; 1524 $this->doc .= ' colspan="'.$colspan.'"'; 1525 } 1526 if($rowspan > 1) { 1527 $this->doc .= ' rowspan="'.$rowspan.'"'; 1528 } 1529 $this->doc .= '>'; 1530 } 1531 1532 /** 1533 * Close a table cell 1534 */ 1535 function tablecell_close() { 1536 $this->doc .= '</td>'; 1537 } 1538 1539 /** 1540 * Returns the current header level. 1541 * (required e.g. by the filelist plugin) 1542 * 1543 * @return int The current header level 1544 */ 1545 function getLastlevel() { 1546 return $this->lastlevel; 1547 } 1548 1549 #region Utility functions 1550 1551 /** 1552 * Build a link 1553 * 1554 * Assembles all parts defined in $link returns HTML for the link 1555 * 1556 * @param array $link attributes of a link 1557 * @return string 1558 * 1559 * @author Andreas Gohr <andi@splitbrain.org> 1560 */ 1561 function _formatLink($link) { 1562 //make sure the url is XHTML compliant (skip mailto) 1563 if(substr($link['url'], 0, 7) != 'mailto:') { 1564 $link['url'] = str_replace('&', '&', $link['url']); 1565 $link['url'] = str_replace('&amp;', '&', $link['url']); 1566 } 1567 //remove double encodings in titles 1568 $link['title'] = str_replace('&amp;', '&', $link['title']); 1569 1570 // be sure there are no bad chars in url or title 1571 // (we can't do this for name because it can contain an img tag) 1572 $link['url'] = strtr($link['url'], array('>' => '%3E', '<' => '%3C', '"' => '%22')); 1573 $link['title'] = strtr($link['title'], array('>' => '>', '<' => '<', '"' => '"')); 1574 1575 $ret = ''; 1576 $ret .= $link['pre']; 1577 $ret .= '<a href="'.$link['url'].'"'; 1578 if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"'; 1579 if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"'; 1580 if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"'; 1581 if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"'; 1582 if(!empty($link['rel'])) $ret .= ' rel="'.trim($link['rel']).'"'; 1583 if(!empty($link['more'])) $ret .= ' '.$link['more']; 1584 $ret .= '>'; 1585 $ret .= $link['name']; 1586 $ret .= '</a>'; 1587 $ret .= $link['suf']; 1588 return $ret; 1589 } 1590 1591 /** 1592 * Renders internal and external media 1593 * 1594 * @author Andreas Gohr <andi@splitbrain.org> 1595 * @param string $src media ID 1596 * @param string $title descriptive text 1597 * @param string $align left|center|right 1598 * @param int $width width of media in pixel 1599 * @param int $height height of media in pixel 1600 * @param string $cache cache|recache|nocache 1601 * @param bool $render should the media be embedded inline or just linked 1602 * @return string 1603 */ 1604 function _media($src, $title = null, $align = null, $width = null, 1605 $height = null, $cache = null, $render = true) { 1606 1607 $ret = ''; 1608 1609 list($ext, $mime) = mimetype($src); 1610 if(substr($mime, 0, 5) == 'image') { 1611 // first get the $title 1612 if(!is_null($title)) { 1613 $title = $this->_xmlEntities($title); 1614 } elseif($ext == 'jpg' || $ext == 'jpeg') { 1615 //try to use the caption from IPTC/EXIF 1616 require_once(DOKU_INC.'inc/JpegMeta.php'); 1617 $jpeg = new JpegMeta(mediaFN($src)); 1618 if($jpeg !== false) $cap = $jpeg->getTitle(); 1619 if(!empty($cap)) { 1620 $title = $this->_xmlEntities($cap); 1621 } 1622 } 1623 if(!$render) { 1624 // if the picture is not supposed to be rendered 1625 // return the title of the picture 1626 if(!$title) { 1627 // just show the sourcename 1628 $title = $this->_xmlEntities(utf8_basename(noNS($src))); 1629 } 1630 return $title; 1631 } 1632 //add image tag 1633 $ret .= '<img src="'.ml($src, array('w' => $width, 'h' => $height, 'cache' => $cache, 'rev'=>$this->_getLastMediaRevisionAt($src))).'"'; 1634 $ret .= ' class="media'.$align.'"'; 1635 1636 if($title) { 1637 $ret .= ' title="'.$title.'"'; 1638 $ret .= ' alt="'.$title.'"'; 1639 } else { 1640 $ret .= ' alt=""'; 1641 } 1642 1643 if(!is_null($width)) 1644 $ret .= ' width="'.$this->_xmlEntities($width).'"'; 1645 1646 if(!is_null($height)) 1647 $ret .= ' height="'.$this->_xmlEntities($height).'"'; 1648 1649 $ret .= ' />'; 1650 1651 } elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')) { 1652 // first get the $title 1653 $title = !is_null($title) ? $this->_xmlEntities($title) : false; 1654 if(!$render) { 1655 // if the file is not supposed to be rendered 1656 // return the title of the file (just the sourcename if there is no title) 1657 return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src))); 1658 } 1659 1660 $att = array(); 1661 $att['class'] = "media$align"; 1662 if($title) { 1663 $att['title'] = $title; 1664 } 1665 1666 if(media_supportedav($mime, 'video')) { 1667 //add video 1668 $ret .= $this->_video($src, $width, $height, $att); 1669 } 1670 if(media_supportedav($mime, 'audio')) { 1671 //add audio 1672 $ret .= $this->_audio($src, $att); 1673 } 1674 1675 } elseif($mime == 'application/x-shockwave-flash') { 1676 if(!$render) { 1677 // if the flash is not supposed to be rendered 1678 // return the title of the flash 1679 if(!$title) { 1680 // just show the sourcename 1681 $title = utf8_basename(noNS($src)); 1682 } 1683 return $this->_xmlEntities($title); 1684 } 1685 1686 $att = array(); 1687 $att['class'] = "media$align"; 1688 if($align == 'right') $att['align'] = 'right'; 1689 if($align == 'left') $att['align'] = 'left'; 1690 $ret .= html_flashobject( 1691 ml($src, array('cache' => $cache), true, '&'), $width, $height, 1692 array('quality' => 'high'), 1693 null, 1694 $att, 1695 $this->_xmlEntities($title) 1696 ); 1697 } elseif($title) { 1698 // well at least we have a title to display 1699 $ret .= $this->_xmlEntities($title); 1700 } else { 1701 // just show the sourcename 1702 $ret .= $this->_xmlEntities(utf8_basename(noNS($src))); 1703 } 1704 1705 return $ret; 1706 } 1707 1708 /** 1709 * Escape string for output 1710 * 1711 * @param $string 1712 * @return string 1713 */ 1714 function _xmlEntities($string) { 1715 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 1716 } 1717 1718 /** 1719 * Creates a linkid from a headline 1720 * 1721 * @author Andreas Gohr <andi@splitbrain.org> 1722 * @param string $title The headline title 1723 * @param boolean $create Create a new unique ID? 1724 * @return string 1725 */ 1726 function _headerToLink($title, $create = false) { 1727 if($create) { 1728 return sectionID($title, $this->headers); 1729 } else { 1730 $check = false; 1731 return sectionID($title, $check); 1732 } 1733 } 1734 1735 /** 1736 * Construct a title and handle images in titles 1737 * 1738 * @author Harry Fuecks <hfuecks@gmail.com> 1739 * @param string|array $title either string title or media array 1740 * @param string $default default title if nothing else is found 1741 * @param bool $isImage will be set to true if it's a media file 1742 * @param null|string $id linked page id (used to extract title from first heading) 1743 * @param string $linktype content|navigation 1744 * @return string HTML of the title, might be full image tag or just escaped text 1745 */ 1746 function _getLinkTitle($title, $default, &$isImage, $id = null, $linktype = 'content') { 1747 $isImage = false; 1748 if(is_array($title)) { 1749 $isImage = true; 1750 return $this->_imageTitle($title); 1751 } elseif(is_null($title) || trim($title) == '') { 1752 if(useHeading($linktype) && $id) { 1753 $heading = p_get_first_heading($id); 1754 if(!blank($heading)) { 1755 return $this->_xmlEntities($heading); 1756 } 1757 } 1758 return $this->_xmlEntities($default); 1759 } else { 1760 return $this->_xmlEntities($title); 1761 } 1762 } 1763 1764 /** 1765 * Returns HTML code for images used in link titles 1766 * 1767 * @author Andreas Gohr <andi@splitbrain.org> 1768 * @param array $img 1769 * @return string HTML img tag or similar 1770 */ 1771 function _imageTitle($img) { 1772 global $ID; 1773 1774 // some fixes on $img['src'] 1775 // see internalmedia() and externalmedia() 1776 list($img['src']) = explode('#', $img['src'], 2); 1777 if($img['type'] == 'internalmedia') { 1778 resolve_mediaid(getNS($ID), $img['src'], $exists ,$this->date_at, true); 1779 } 1780 1781 return $this->_media( 1782 $img['src'], 1783 $img['title'], 1784 $img['align'], 1785 $img['width'], 1786 $img['height'], 1787 $img['cache'] 1788 ); 1789 } 1790 1791 /** 1792 * helperfunction to return a basic link to a media 1793 * 1794 * used in internalmedia() and externalmedia() 1795 * 1796 * @author Pierre Spring <pierre.spring@liip.ch> 1797 * @param string $src media ID 1798 * @param string $title descriptive text 1799 * @param string $align left|center|right 1800 * @param int $width width of media in pixel 1801 * @param int $height height of media in pixel 1802 * @param string $cache cache|recache|nocache 1803 * @param bool $render should the media be embedded inline or just linked 1804 * @return array associative array with link config 1805 */ 1806 function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) { 1807 global $conf; 1808 1809 $link = array(); 1810 $link['class'] = 'media'; 1811 $link['style'] = ''; 1812 $link['pre'] = ''; 1813 $link['suf'] = ''; 1814 $link['more'] = ''; 1815 $link['target'] = $conf['target']['media']; 1816 if($conf['target']['media']) $link['rel'] = 'noopener'; 1817 $link['title'] = $this->_xmlEntities($src); 1818 $link['name'] = $this->_media($src, $title, $align, $width, $height, $cache, $render); 1819 1820 return $link; 1821 } 1822 1823 /** 1824 * Embed video(s) in HTML 1825 * 1826 * @author Anika Henke <anika@selfthinker.org> 1827 * @author Schplurtz le Déboulonné <Schplurtz@laposte.net> 1828 * 1829 * @param string $src - ID of video to embed 1830 * @param int $width - width of the video in pixels 1831 * @param int $height - height of the video in pixels 1832 * @param array $atts - additional attributes for the <video> tag 1833 * @return string 1834 */ 1835 function _video($src, $width, $height, $atts = null) { 1836 // prepare width and height 1837 if(is_null($atts)) $atts = array(); 1838 $atts['width'] = (int) $width; 1839 $atts['height'] = (int) $height; 1840 if(!$atts['width']) $atts['width'] = 320; 1841 if(!$atts['height']) $atts['height'] = 240; 1842 1843 $posterUrl = ''; 1844 $files = array(); 1845 $tracks = array(); 1846 $isExternal = media_isexternal($src); 1847 1848 if ($isExternal) { 1849 // take direct source for external files 1850 list(/*ext*/, $srcMime) = mimetype($src); 1851 $files[$srcMime] = $src; 1852 } else { 1853 // prepare alternative formats 1854 $extensions = array('webm', 'ogv', 'mp4'); 1855 $files = media_alternativefiles($src, $extensions); 1856 $poster = media_alternativefiles($src, array('jpg', 'png')); 1857 $tracks = media_trackfiles($src); 1858 if(!empty($poster)) { 1859 $posterUrl = ml(reset($poster), '', true, '&'); 1860 } 1861 } 1862 1863 $out = ''; 1864 // open video tag 1865 $out .= '<video '.buildAttributes($atts).' controls="controls"'; 1866 if($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"'; 1867 $out .= '>'.NL; 1868 $fallback = ''; 1869 1870 // output source for each alternative video format 1871 foreach($files as $mime => $file) { 1872 if ($isExternal) { 1873 $url = $file; 1874 $linkType = 'externalmedia'; 1875 } else { 1876 $url = ml($file, '', true, '&'); 1877 $linkType = 'internalmedia'; 1878 } 1879 $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file))); 1880 1881 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1882 // alternative content (just a link to the file) 1883 $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true); 1884 } 1885 1886 // output each track if any 1887 foreach( $tracks as $trackid => $info ) { 1888 list( $kind, $srclang ) = array_map( 'hsc', $info ); 1889 $out .= "<track kind=\"$kind\" srclang=\"$srclang\" "; 1890 $out .= "label=\"$srclang\" "; 1891 $out .= 'src="'.ml($trackid, '', true).'">'.NL; 1892 } 1893 1894 // finish 1895 $out .= $fallback; 1896 $out .= '</video>'.NL; 1897 return $out; 1898 } 1899 1900 /** 1901 * Embed audio in HTML 1902 * 1903 * @author Anika Henke <anika@selfthinker.org> 1904 * 1905 * @param string $src - ID of audio to embed 1906 * @param array $atts - additional attributes for the <audio> tag 1907 * @return string 1908 */ 1909 function _audio($src, $atts = array()) { 1910 $files = array(); 1911 $isExternal = media_isexternal($src); 1912 1913 if ($isExternal) { 1914 // take direct source for external files 1915 list(/*ext*/, $srcMime) = mimetype($src); 1916 $files[$srcMime] = $src; 1917 } else { 1918 // prepare alternative formats 1919 $extensions = array('ogg', 'mp3', 'wav'); 1920 $files = media_alternativefiles($src, $extensions); 1921 } 1922 1923 $out = ''; 1924 // open audio tag 1925 $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL; 1926 $fallback = ''; 1927 1928 // output source for each alternative audio format 1929 foreach($files as $mime => $file) { 1930 if ($isExternal) { 1931 $url = $file; 1932 $linkType = 'externalmedia'; 1933 } else { 1934 $url = ml($file, '', true, '&'); 1935 $linkType = 'internalmedia'; 1936 } 1937 $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file))); 1938 1939 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1940 // alternative content (just a link to the file) 1941 $fallback .= $this->$linkType($file, $title, null, null, null, $cache = null, $linking = 'linkonly', $return = true); 1942 } 1943 1944 // finish 1945 $out .= $fallback; 1946 $out .= '</audio>'.NL; 1947 return $out; 1948 } 1949 1950 /** 1951 * _getLastMediaRevisionAt is a helperfunction to internalmedia() and _media() 1952 * which returns an existing media revision less or equal to rev or date_at 1953 * 1954 * @author lisps 1955 * @param string $media_id 1956 * @access protected 1957 * @return string revision ('' for current) 1958 */ 1959 function _getLastMediaRevisionAt($media_id){ 1960 if(!$this->date_at || media_isexternal($media_id)) return ''; 1961 $pagelog = new MediaChangeLog($media_id); 1962 return $pagelog->getLastRevisionAt($this->date_at); 1963 } 1964 1965 #endregion 1966} 1967 1968//Setup VIM: ex: et ts=4 : 1969