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