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 Renderer 22 */ 23class Doku_Renderer_xhtml extends Doku_Renderer { 24 25 // @access public 26 var $doc = ''; // will contain the whole document 27 var $toc = array(); // will contain the Table of Contents 28 29 var $sectionedits = array(); // A stack of section edit data 30 private $lastsecid = 0; // last section edit id, used by startSectionEdit 31 32 var $headers = array(); 33 /** @var array a list of footnotes, list starts at 1! */ 34 var $footnotes = array(); 35 var $lastlevel = 0; 36 var $node = array(0,0,0,0,0); 37 var $store = ''; 38 39 var $_counter = array(); // used as global counter, introduced for table classes 40 var $_codeblock = 0; // counts the code and file blocks, used to provide download links 41 42 /** 43 * Register a new edit section range 44 * 45 * @param $type string The section type identifier 46 * @param $title string The section title 47 * @param $start int The byte position for the edit start 48 * @return string A marker class for the starting HTML element 49 * @author Adrian Lang <lang@cosmocode.de> 50 */ 51 public function startSectionEdit($start, $type, $title = null) { 52 $this->sectionedits[] = array(++$this->lastsecid, $start, $type, $title); 53 return 'sectionedit' . $this->lastsecid; 54 } 55 56 /** 57 * Finish an edit section range 58 * 59 * @param $end int The byte position for the edit end; null for the rest of 60 * the page 61 * @author Adrian Lang <lang@cosmocode.de> 62 */ 63 public function finishSectionEdit($end = null) { 64 list($id, $start, $type, $title) = array_pop($this->sectionedits); 65 if (!is_null($end) && $end <= $start) { 66 return; 67 } 68 $this->doc .= "<!-- EDIT$id " . strtoupper($type) . ' '; 69 if (!is_null($title)) { 70 $this->doc .= '"' . str_replace('"', '', $title) . '" '; 71 } 72 $this->doc .= "[$start-" . (is_null($end) ? '' : $end) . '] -->'; 73 } 74 75 function getFormat(){ 76 return 'xhtml'; 77 } 78 79 80 function document_start() { 81 //reset some internals 82 $this->toc = array(); 83 $this->headers = array(); 84 } 85 86 function document_end() { 87 // Finish open section edits. 88 while (count($this->sectionedits) > 0) { 89 if ($this->sectionedits[count($this->sectionedits) - 1][1] <= 1) { 90 // If there is only one section, do not write a section edit 91 // marker. 92 array_pop($this->sectionedits); 93 } else { 94 $this->finishSectionEdit(); 95 } 96 } 97 98 if ( count ($this->footnotes) > 0 ) { 99 $this->doc .= '<div class="footnotes">'.DOKU_LF; 100 101 foreach ( $this->footnotes as $id => $footnote ) { 102 // check its not a placeholder that indicates actual footnote text is elsewhere 103 if (substr($footnote, 0, 5) != "@@FNT") { 104 105 // open the footnote and set the anchor and backlink 106 $this->doc .= '<div class="fn">'; 107 $this->doc .= '<sup><a href="#fnt__'.$id.'" id="fn__'.$id.'" class="fn_bot">'; 108 $this->doc .= $id.')</a></sup> '.DOKU_LF; 109 110 // get any other footnotes that use the same markup 111 $alt = array_keys($this->footnotes, "@@FNT$id"); 112 113 if (count($alt)) { 114 foreach ($alt as $ref) { 115 // set anchor and backlink for the other footnotes 116 $this->doc .= ', <sup><a href="#fnt__'.($ref).'" id="fn__'.($ref).'" class="fn_bot">'; 117 $this->doc .= ($ref).')</a></sup> '.DOKU_LF; 118 } 119 } 120 121 // add footnote markup and close this footnote 122 $this->doc .= $footnote; 123 $this->doc .= '</div>' . DOKU_LF; 124 } 125 } 126 $this->doc .= '</div>'.DOKU_LF; 127 } 128 129 // Prepare the TOC 130 global $conf; 131 if($this->info['toc'] && is_array($this->toc) && $conf['tocminheads'] && count($this->toc) >= $conf['tocminheads']){ 132 global $TOC; 133 $TOC = $this->toc; 134 } 135 136 // make sure there are no empty paragraphs 137 $this->doc = preg_replace('#<p>\s*</p>#','',$this->doc); 138 } 139 140 function toc_additem($id, $text, $level) { 141 global $conf; 142 143 //handle TOC 144 if($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']){ 145 $this->toc[] = html_mktocitem($id, $text, $level-$conf['toptoclevel']+1); 146 } 147 } 148 149 function header($text, $level, $pos) { 150 global $conf; 151 152 if(!$text) return; //skip empty headlines 153 154 $hid = $this->_headerToLink($text,true); 155 156 //only add items within configured levels 157 $this->toc_additem($hid, $text, $level); 158 159 // adjust $node to reflect hierarchy of levels 160 $this->node[$level-1]++; 161 if ($level < $this->lastlevel) { 162 for ($i = 0; $i < $this->lastlevel-$level; $i++) { 163 $this->node[$this->lastlevel-$i-1] = 0; 164 } 165 } 166 $this->lastlevel = $level; 167 168 if ($level <= $conf['maxseclevel'] && 169 count($this->sectionedits) > 0 && 170 $this->sectionedits[count($this->sectionedits) - 1][2] === 'section') { 171 $this->finishSectionEdit($pos - 1); 172 } 173 174 // write the header 175 $this->doc .= DOKU_LF.'<h'.$level; 176 if ($level <= $conf['maxseclevel']) { 177 $this->doc .= ' class="' . $this->startSectionEdit($pos, 'section', $text) . '"'; 178 } 179 $this->doc .= ' id="'.$hid.'">'; 180 $this->doc .= $this->_xmlEntities($text); 181 $this->doc .= "</h$level>".DOKU_LF; 182 } 183 184 function section_open($level) { 185 $this->doc .= '<div class="level' . $level . '">' . DOKU_LF; 186 } 187 188 function section_close() { 189 $this->doc .= DOKU_LF.'</div>'.DOKU_LF; 190 } 191 192 function cdata($text) { 193 $this->doc .= $this->_xmlEntities($text); 194 } 195 196 function p_open() { 197 $this->doc .= DOKU_LF.'<p>'.DOKU_LF; 198 } 199 200 function p_close() { 201 $this->doc .= DOKU_LF.'</p>'.DOKU_LF; 202 } 203 204 function linebreak() { 205 $this->doc .= '<br/>'.DOKU_LF; 206 } 207 208 function hr() { 209 $this->doc .= '<hr />'.DOKU_LF; 210 } 211 212 function strong_open() { 213 $this->doc .= '<strong>'; 214 } 215 216 function strong_close() { 217 $this->doc .= '</strong>'; 218 } 219 220 function emphasis_open() { 221 $this->doc .= '<em>'; 222 } 223 224 function emphasis_close() { 225 $this->doc .= '</em>'; 226 } 227 228 function underline_open() { 229 $this->doc .= '<em class="u">'; 230 } 231 232 function underline_close() { 233 $this->doc .= '</em>'; 234 } 235 236 function monospace_open() { 237 $this->doc .= '<code>'; 238 } 239 240 function monospace_close() { 241 $this->doc .= '</code>'; 242 } 243 244 function subscript_open() { 245 $this->doc .= '<sub>'; 246 } 247 248 function subscript_close() { 249 $this->doc .= '</sub>'; 250 } 251 252 function superscript_open() { 253 $this->doc .= '<sup>'; 254 } 255 256 function superscript_close() { 257 $this->doc .= '</sup>'; 258 } 259 260 function deleted_open() { 261 $this->doc .= '<del>'; 262 } 263 264 function deleted_close() { 265 $this->doc .= '</del>'; 266 } 267 268 /** 269 * Callback for footnote start syntax 270 * 271 * All following content will go to the footnote instead of 272 * the document. To achieve this the previous rendered content 273 * is moved to $store and $doc is cleared 274 * 275 * @author Andreas Gohr <andi@splitbrain.org> 276 */ 277 function footnote_open() { 278 279 // move current content to store and record footnote 280 $this->store = $this->doc; 281 $this->doc = ''; 282 } 283 284 /** 285 * Callback for footnote end syntax 286 * 287 * All rendered content is moved to the $footnotes array and the old 288 * content is restored from $store again 289 * 290 * @author Andreas Gohr 291 */ 292 function footnote_close() { 293 /** @var $fnid int takes track of seen footnotes, assures they are unique even across multiple docs FS#2841 */ 294 static $fnid = 0; 295 // assign new footnote id (we start at 1) 296 $fnid++; 297 298 // recover footnote into the stack and restore old content 299 $footnote = $this->doc; 300 $this->doc = $this->store; 301 $this->store = ''; 302 303 // check to see if this footnote has been seen before 304 $i = array_search($footnote, $this->footnotes); 305 306 if ($i === false) { 307 // its a new footnote, add it to the $footnotes array 308 $this->footnotes[$fnid] = $footnote; 309 } else { 310 // seen this one before, save a placeholder 311 $this->footnotes[$fnid] = "@@FNT".($i); 312 } 313 314 // output the footnote reference and link 315 $this->doc .= '<sup><a href="#fn__'.$fnid.'" id="fnt__'.$fnid.'" class="fn_top">'.$fnid.')</a></sup>'; 316 } 317 318 function listu_open() { 319 $this->doc .= '<ul>'.DOKU_LF; 320 } 321 322 function listu_close() { 323 $this->doc .= '</ul>'.DOKU_LF; 324 } 325 326 function listo_open() { 327 $this->doc .= '<ol>'.DOKU_LF; 328 } 329 330 function listo_close() { 331 $this->doc .= '</ol>'.DOKU_LF; 332 } 333 334 function listitem_open($level) { 335 $this->doc .= '<li class="level'.$level.'">'; 336 } 337 338 function listitem_close() { 339 $this->doc .= '</li>'.DOKU_LF; 340 } 341 342 function listcontent_open() { 343 $this->doc .= '<div class="li">'; 344 } 345 346 function listcontent_close() { 347 $this->doc .= '</div>'.DOKU_LF; 348 } 349 350 function unformatted($text) { 351 $this->doc .= $this->_xmlEntities($text); 352 } 353 354 /** 355 * Execute PHP code if allowed 356 * 357 * @param string $text PHP code that is either executed or printed 358 * @param string $wrapper html element to wrap result if $conf['phpok'] is okff 359 * 360 * @author Andreas Gohr <andi@splitbrain.org> 361 */ 362 function php($text, $wrapper='code') { 363 global $conf; 364 365 if($conf['phpok']){ 366 ob_start(); 367 eval($text); 368 $this->doc .= ob_get_contents(); 369 ob_end_clean(); 370 } else { 371 $this->doc .= p_xhtml_cached_geshi($text, 'php', $wrapper); 372 } 373 } 374 375 function phpblock($text) { 376 $this->php($text, 'pre'); 377 } 378 379 /** 380 * Insert HTML if allowed 381 * 382 * @param string $text html text 383 * @param string $wrapper html element to wrap result if $conf['htmlok'] is okff 384 * 385 * @author Andreas Gohr <andi@splitbrain.org> 386 */ 387 function html($text, $wrapper='code') { 388 global $conf; 389 390 if($conf['htmlok']){ 391 $this->doc .= $text; 392 } else { 393 $this->doc .= p_xhtml_cached_geshi($text, 'html4strict', $wrapper); 394 } 395 } 396 397 function htmlblock($text) { 398 $this->html($text, 'pre'); 399 } 400 401 function quote_open() { 402 $this->doc .= '<blockquote><div class="no">'.DOKU_LF; 403 } 404 405 function quote_close() { 406 $this->doc .= '</div></blockquote>'.DOKU_LF; 407 } 408 409 function preformatted($text) { 410 $this->doc .= '<pre class="code">' . trim($this->_xmlEntities($text),"\n\r") . '</pre>'. DOKU_LF; 411 } 412 413 function file($text, $language=null, $filename=null) { 414 $this->_highlight('file',$text,$language,$filename); 415 } 416 417 function code($text, $language=null, $filename=null) { 418 $this->_highlight('code',$text,$language,$filename); 419 } 420 421 /** 422 * Use GeSHi to highlight language syntax in code and file blocks 423 * 424 * @author Andreas Gohr <andi@splitbrain.org> 425 */ 426 function _highlight($type, $text, $language=null, $filename=null) { 427 global $conf; 428 global $ID; 429 global $lang; 430 431 if($filename){ 432 // add icon 433 list($ext) = mimetype($filename,false); 434 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext); 435 $class = 'mediafile mf_'.$class; 436 437 $this->doc .= '<dl class="'.$type.'">'.DOKU_LF; 438 $this->doc .= '<dt><a href="'.exportlink($ID,'code',array('codeblock'=>$this->_codeblock)).'" title="'.$lang['download'].'" class="'.$class.'">'; 439 $this->doc .= hsc($filename); 440 $this->doc .= '</a></dt>'.DOKU_LF.'<dd>'; 441 } 442 443 if ($text{0} == "\n") { 444 $text = substr($text, 1); 445 } 446 if (substr($text, -1) == "\n") { 447 $text = substr($text, 0, -1); 448 } 449 450 if ( is_null($language) ) { 451 $this->doc .= '<pre class="'.$type.'">'.$this->_xmlEntities($text).'</pre>'.DOKU_LF; 452 } else { 453 $class = 'code'; //we always need the code class to make the syntax highlighting apply 454 if($type != 'code') $class .= ' '.$type; 455 456 $this->doc .= "<pre class=\"$class $language\">".p_xhtml_cached_geshi($text, $language, '').'</pre>'.DOKU_LF; 457 } 458 459 if($filename){ 460 $this->doc .= '</dd></dl>'.DOKU_LF; 461 } 462 463 $this->_codeblock++; 464 } 465 466 function acronym($acronym) { 467 468 if ( array_key_exists($acronym, $this->acronyms) ) { 469 470 $title = $this->_xmlEntities($this->acronyms[$acronym]); 471 472 $this->doc .= '<abbr title="'.$title 473 .'">'.$this->_xmlEntities($acronym).'</abbr>'; 474 475 } else { 476 $this->doc .= $this->_xmlEntities($acronym); 477 } 478 } 479 480 function smiley($smiley) { 481 if ( array_key_exists($smiley, $this->smileys) ) { 482 $title = $this->_xmlEntities($this->smileys[$smiley]); 483 $this->doc .= '<img src="'.DOKU_BASE.'lib/images/smileys/'.$this->smileys[$smiley]. 484 '" class="icon" alt="'. 485 $this->_xmlEntities($smiley).'" />'; 486 } else { 487 $this->doc .= $this->_xmlEntities($smiley); 488 } 489 } 490 491 /* 492 * not used 493 function wordblock($word) { 494 if ( array_key_exists($word, $this->badwords) ) { 495 $this->doc .= '** BLEEP **'; 496 } else { 497 $this->doc .= $this->_xmlEntities($word); 498 } 499 } 500 */ 501 502 function entity($entity) { 503 if ( array_key_exists($entity, $this->entities) ) { 504 $this->doc .= $this->entities[$entity]; 505 } else { 506 $this->doc .= $this->_xmlEntities($entity); 507 } 508 } 509 510 function multiplyentity($x, $y) { 511 $this->doc .= "$x×$y"; 512 } 513 514 function singlequoteopening() { 515 global $lang; 516 $this->doc .= $lang['singlequoteopening']; 517 } 518 519 function singlequoteclosing() { 520 global $lang; 521 $this->doc .= $lang['singlequoteclosing']; 522 } 523 524 function apostrophe() { 525 global $lang; 526 $this->doc .= $lang['apostrophe']; 527 } 528 529 function doublequoteopening() { 530 global $lang; 531 $this->doc .= $lang['doublequoteopening']; 532 } 533 534 function doublequoteclosing() { 535 global $lang; 536 $this->doc .= $lang['doublequoteclosing']; 537 } 538 539 /** 540 */ 541 function camelcaselink($link) { 542 $this->internallink($link,$link); 543 } 544 545 546 function locallink($hash, $name = null){ 547 global $ID; 548 $name = $this->_getLinkTitle($name, $hash, $isImage); 549 $hash = $this->_headerToLink($hash); 550 $title = $ID.' ↵'; 551 $this->doc .= '<a href="#'.$hash.'" title="'.$title.'" class="wikilink1">'; 552 $this->doc .= $name; 553 $this->doc .= '</a>'; 554 } 555 556 /** 557 * Render an internal Wiki Link 558 * 559 * $search,$returnonly & $linktype are not for the renderer but are used 560 * elsewhere - no need to implement them in other renderers 561 * 562 * @param string $id pageid 563 * @param string|null $name link name 564 * @param string|null $search adds search url param 565 * @param bool $returnonly whether to return html or write to doc attribute 566 * @param string $linktype type to set use of headings 567 * @return void|string writes to doc attribute or returns html depends on $returnonly 568 * @author Andreas Gohr <andi@splitbrain.org> 569 */ 570 function internallink($id, $name = null, $search=null,$returnonly=false,$linktype='content') { 571 global $conf; 572 global $ID; 573 global $INFO; 574 575 $params = ''; 576 $parts = explode('?', $id, 2); 577 if (count($parts) === 2) { 578 $id = $parts[0]; 579 $params = $parts[1]; 580 } 581 582 // For empty $id we need to know the current $ID 583 // We need this check because _simpleTitle needs 584 // correct $id and resolve_pageid() use cleanID($id) 585 // (some things could be lost) 586 if ($id === '') { 587 $id = $ID; 588 } 589 590 // default name is based on $id as given 591 $default = $this->_simpleTitle($id); 592 593 // now first resolve and clean up the $id 594 resolve_pageid(getNS($ID),$id,$exists); 595 596 $name = $this->_getLinkTitle($name, $default, $isImage, $id, $linktype); 597 if ( !$isImage ) { 598 if ( $exists ) { 599 $class='wikilink1'; 600 } else { 601 $class='wikilink2'; 602 $link['rel']='nofollow'; 603 } 604 } else { 605 $class='media'; 606 } 607 608 //keep hash anchor 609 @list($id,$hash) = explode('#',$id,2); 610 if(!empty($hash)) $hash = $this->_headerToLink($hash); 611 612 //prepare for formating 613 $link['target'] = $conf['target']['wiki']; 614 $link['style'] = ''; 615 $link['pre'] = ''; 616 $link['suf'] = ''; 617 // highlight link to current page 618 if ($id == $INFO['id']) { 619 $link['pre'] = '<span class="curid">'; 620 $link['suf'] = '</span>'; 621 } 622 $link['more'] = ''; 623 $link['class'] = $class; 624 $link['url'] = wl($id, $params); 625 $link['name'] = $name; 626 $link['title'] = $id; 627 //add search string 628 if($search){ 629 ($conf['userewrite']) ? $link['url'].='?' : $link['url'].='&'; 630 if(is_array($search)){ 631 $search = array_map('rawurlencode',$search); 632 $link['url'] .= 's[]='.join('&s[]=',$search); 633 }else{ 634 $link['url'] .= 's='.rawurlencode($search); 635 } 636 } 637 638 //keep hash 639 if($hash) $link['url'].='#'.$hash; 640 641 //output formatted 642 if($returnonly){ 643 return $this->_formatLink($link); 644 }else{ 645 $this->doc .= $this->_formatLink($link); 646 } 647 } 648 649 function externallink($url, $name = null) { 650 global $conf; 651 652 $name = $this->_getLinkTitle($name, $url, $isImage); 653 654 // url might be an attack vector, only allow registered protocols 655 if(is_null($this->schemes)) $this->schemes = getSchemes(); 656 list($scheme) = explode('://',$url); 657 $scheme = strtolower($scheme); 658 if(!in_array($scheme,$this->schemes)) $url = ''; 659 660 // is there still an URL? 661 if(!$url){ 662 $this->doc .= $name; 663 return; 664 } 665 666 // set class 667 if ( !$isImage ) { 668 $class='urlextern'; 669 } else { 670 $class='media'; 671 } 672 673 //prepare for formating 674 $link['target'] = $conf['target']['extern']; 675 $link['style'] = ''; 676 $link['pre'] = ''; 677 $link['suf'] = ''; 678 $link['more'] = ''; 679 $link['class'] = $class; 680 $link['url'] = $url; 681 682 $link['name'] = $name; 683 $link['title'] = $this->_xmlEntities($url); 684 if($conf['relnofollow']) $link['more'] .= ' rel="nofollow"'; 685 686 //output formatted 687 $this->doc .= $this->_formatLink($link); 688 } 689 690 /** 691 */ 692 function interwikilink($match, $name = null, $wikiName, $wikiUri) { 693 global $conf; 694 695 $link = array(); 696 $link['target'] = $conf['target']['interwiki']; 697 $link['pre'] = ''; 698 $link['suf'] = ''; 699 $link['more'] = ''; 700 $link['name'] = $this->_getLinkTitle($name, $wikiUri, $isImage); 701 702 //get interwiki URL 703 $url = $this->_resolveInterWiki($wikiName,$wikiUri); 704 705 if ( !$isImage ) { 706 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$wikiName); 707 $link['class'] = "interwiki iw_$class"; 708 } else { 709 $link['class'] = 'media'; 710 } 711 712 //do we stay at the same server? Use local target 713 if( strpos($url,DOKU_URL) === 0 ){ 714 $link['target'] = $conf['target']['wiki']; 715 } 716 717 $link['url'] = $url; 718 $link['title'] = htmlspecialchars($link['url']); 719 720 //output formatted 721 $this->doc .= $this->_formatLink($link); 722 } 723 724 /** 725 */ 726 function windowssharelink($url, $name = null) { 727 global $conf; 728 global $lang; 729 //simple setup 730 $link['target'] = $conf['target']['windows']; 731 $link['pre'] = ''; 732 $link['suf'] = ''; 733 $link['style'] = ''; 734 735 $link['name'] = $this->_getLinkTitle($name, $url, $isImage); 736 if ( !$isImage ) { 737 $link['class'] = 'windows'; 738 } else { 739 $link['class'] = 'media'; 740 } 741 742 $link['title'] = $this->_xmlEntities($url); 743 $url = str_replace('\\','/',$url); 744 $url = 'file:///'.$url; 745 $link['url'] = $url; 746 747 //output formatted 748 $this->doc .= $this->_formatLink($link); 749 } 750 751 function emaillink($address, $name = null) { 752 global $conf; 753 //simple setup 754 $link = array(); 755 $link['target'] = ''; 756 $link['pre'] = ''; 757 $link['suf'] = ''; 758 $link['style'] = ''; 759 $link['more'] = ''; 760 761 $name = $this->_getLinkTitle($name, '', $isImage); 762 if ( !$isImage ) { 763 $link['class']='mail'; 764 } else { 765 $link['class']='media'; 766 } 767 768 $address = $this->_xmlEntities($address); 769 $address = obfuscate($address); 770 $title = $address; 771 772 if(empty($name)){ 773 $name = $address; 774 } 775 776 if($conf['mailguard'] == 'visible') $address = rawurlencode($address); 777 778 $link['url'] = 'mailto:'.$address; 779 $link['name'] = $name; 780 $link['title'] = $title; 781 782 //output formatted 783 $this->doc .= $this->_formatLink($link); 784 } 785 786 function internalmedia ($src, $title=null, $align=null, $width=null, 787 $height=null, $cache=null, $linking=null, $return=NULL) { 788 global $ID; 789 list($src,$hash) = explode('#',$src,2); 790 resolve_mediaid(getNS($ID),$src, $exists); 791 792 $noLink = false; 793 $render = ($linking == 'linkonly') ? false : true; 794 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 795 796 list($ext,$mime,$dl) = mimetype($src,false); 797 if(substr($mime,0,5) == 'image' && $render){ 798 $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),($linking=='direct')); 799 }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){ 800 // don't link movies 801 $noLink = true; 802 }else{ 803 // add file icons 804 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext); 805 $link['class'] .= ' mediafile mf_'.$class; 806 $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),true); 807 if ($exists) $link['title'] .= ' (' . filesize_h(filesize(mediaFN($src))).')'; 808 } 809 810 if($hash) $link['url'] .= '#'.$hash; 811 812 //markup non existing files 813 if (!$exists) { 814 $link['class'] .= ' wikilink2'; 815 } 816 817 //output formatted 818 if ($return) { 819 if ($linking == 'nolink' || $noLink) return $link['name']; 820 else return $this->_formatLink($link); 821 } else { 822 if ($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 823 else $this->doc .= $this->_formatLink($link); 824 } 825 } 826 827 function externalmedia ($src, $title=null, $align=null, $width=null, 828 $height=null, $cache=null, $linking=null) { 829 list($src,$hash) = explode('#',$src,2); 830 $noLink = false; 831 $render = ($linking == 'linkonly') ? false : true; 832 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 833 834 $link['url'] = ml($src,array('cache'=>$cache)); 835 836 list($ext,$mime,$dl) = mimetype($src,false); 837 if(substr($mime,0,5) == 'image' && $render){ 838 // link only jpeg images 839 // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true; 840 }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){ 841 // don't link movies 842 $noLink = true; 843 }else{ 844 // add file icons 845 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext); 846 $link['class'] .= ' mediafile mf_'.$class; 847 } 848 849 if($hash) $link['url'] .= '#'.$hash; 850 851 //output formatted 852 if ($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 853 else $this->doc .= $this->_formatLink($link); 854 } 855 856 /** 857 * Renders an RSS feed 858 * 859 * @author Andreas Gohr <andi@splitbrain.org> 860 */ 861 function rss ($url,$params){ 862 global $lang; 863 global $conf; 864 865 require_once(DOKU_INC.'inc/FeedParser.php'); 866 $feed = new FeedParser(); 867 $feed->set_feed_url($url); 868 869 //disable warning while fetching 870 if (!defined('DOKU_E_LEVEL')) { $elvl = error_reporting(E_ERROR); } 871 $rc = $feed->init(); 872 if (!defined('DOKU_E_LEVEL')) { error_reporting($elvl); } 873 874 //decide on start and end 875 if($params['reverse']){ 876 $mod = -1; 877 $start = $feed->get_item_quantity()-1; 878 $end = $start - ($params['max']); 879 $end = ($end < -1) ? -1 : $end; 880 }else{ 881 $mod = 1; 882 $start = 0; 883 $end = $feed->get_item_quantity(); 884 $end = ($end > $params['max']) ? $params['max'] : $end; 885 } 886 887 $this->doc .= '<ul class="rss">'; 888 if($rc){ 889 for ($x = $start; $x != $end; $x += $mod) { 890 $item = $feed->get_item($x); 891 $this->doc .= '<li><div class="li">'; 892 // support feeds without links 893 $lnkurl = $item->get_permalink(); 894 if($lnkurl){ 895 // title is escaped by SimplePie, we unescape here because it 896 // is escaped again in externallink() FS#1705 897 $this->externallink($item->get_permalink(), 898 html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')); 899 }else{ 900 $this->doc .= ' '.$item->get_title(); 901 } 902 if($params['author']){ 903 $author = $item->get_author(0); 904 if($author){ 905 $name = $author->get_name(); 906 if(!$name) $name = $author->get_email(); 907 if($name) $this->doc .= ' '.$lang['by'].' '.$name; 908 } 909 } 910 if($params['date']){ 911 $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')'; 912 } 913 if($params['details']){ 914 $this->doc .= '<div class="detail">'; 915 if($conf['htmlok']){ 916 $this->doc .= $item->get_description(); 917 }else{ 918 $this->doc .= strip_tags($item->get_description()); 919 } 920 $this->doc .= '</div>'; 921 } 922 923 $this->doc .= '</div></li>'; 924 } 925 }else{ 926 $this->doc .= '<li><div class="li">'; 927 $this->doc .= '<em>'.$lang['rssfailed'].'</em>'; 928 $this->externallink($url); 929 if($conf['allowdebug']){ 930 $this->doc .= '<!--'.hsc($feed->error).'-->'; 931 } 932 $this->doc .= '</div></li>'; 933 } 934 $this->doc .= '</ul>'; 935 } 936 937 // $numrows not yet implemented 938 function table_open($maxcols = null, $numrows = null, $pos = null){ 939 global $lang; 940 // initialize the row counter used for classes 941 $this->_counter['row_counter'] = 0; 942 $class = 'table'; 943 if ($pos !== null) { 944 $class .= ' ' . $this->startSectionEdit($pos, 'table'); 945 } 946 $this->doc .= '<div class="' . $class . '"><table class="inline">' . 947 DOKU_LF; 948 } 949 950 function table_close($pos = null){ 951 $this->doc .= '</table></div>'.DOKU_LF; 952 if ($pos !== null) { 953 $this->finishSectionEdit($pos); 954 } 955 } 956 957 function tablethead_open(){ 958 $this->doc .= DOKU_TAB . '<thead>' . DOKU_LF; 959 } 960 961 function tablethead_close(){ 962 $this->doc .= DOKU_TAB . '</thead>' . DOKU_LF; 963 } 964 965 function tablerow_open(){ 966 // initialize the cell counter used for classes 967 $this->_counter['cell_counter'] = 0; 968 $class = 'row' . $this->_counter['row_counter']++; 969 $this->doc .= DOKU_TAB . '<tr class="'.$class.'">' . DOKU_LF . DOKU_TAB . DOKU_TAB; 970 } 971 972 function tablerow_close(){ 973 $this->doc .= DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF; 974 } 975 976 function tableheader_open($colspan = 1, $align = null, $rowspan = 1){ 977 $class = 'class="col' . $this->_counter['cell_counter']++; 978 if ( !is_null($align) ) { 979 $class .= ' '.$align.'align'; 980 } 981 $class .= '"'; 982 $this->doc .= '<th ' . $class; 983 if ( $colspan > 1 ) { 984 $this->_counter['cell_counter'] += $colspan-1; 985 $this->doc .= ' colspan="'.$colspan.'"'; 986 } 987 if ( $rowspan > 1 ) { 988 $this->doc .= ' rowspan="'.$rowspan.'"'; 989 } 990 $this->doc .= '>'; 991 } 992 993 function tableheader_close(){ 994 $this->doc .= '</th>'; 995 } 996 997 function tablecell_open($colspan = 1, $align = null, $rowspan = 1){ 998 $class = 'class="col' . $this->_counter['cell_counter']++; 999 if ( !is_null($align) ) { 1000 $class .= ' '.$align.'align'; 1001 } 1002 $class .= '"'; 1003 $this->doc .= '<td '.$class; 1004 if ( $colspan > 1 ) { 1005 $this->_counter['cell_counter'] += $colspan-1; 1006 $this->doc .= ' colspan="'.$colspan.'"'; 1007 } 1008 if ( $rowspan > 1 ) { 1009 $this->doc .= ' rowspan="'.$rowspan.'"'; 1010 } 1011 $this->doc .= '>'; 1012 } 1013 1014 function tablecell_close(){ 1015 $this->doc .= '</td>'; 1016 } 1017 1018 //---------------------------------------------------------- 1019 // Utils 1020 1021 /** 1022 * Build a link 1023 * 1024 * Assembles all parts defined in $link returns HTML for the link 1025 * 1026 * @author Andreas Gohr <andi@splitbrain.org> 1027 */ 1028 function _formatLink($link){ 1029 //make sure the url is XHTML compliant (skip mailto) 1030 if(substr($link['url'],0,7) != 'mailto:'){ 1031 $link['url'] = str_replace('&','&',$link['url']); 1032 $link['url'] = str_replace('&amp;','&',$link['url']); 1033 } 1034 //remove double encodings in titles 1035 $link['title'] = str_replace('&amp;','&',$link['title']); 1036 1037 // be sure there are no bad chars in url or title 1038 // (we can't do this for name because it can contain an img tag) 1039 $link['url'] = strtr($link['url'],array('>'=>'%3E','<'=>'%3C','"'=>'%22')); 1040 $link['title'] = strtr($link['title'],array('>'=>'>','<'=>'<','"'=>'"')); 1041 1042 $ret = ''; 1043 $ret .= $link['pre']; 1044 $ret .= '<a href="'.$link['url'].'"'; 1045 if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"'; 1046 if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"'; 1047 if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"'; 1048 if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"'; 1049 if(!empty($link['rel'])) $ret .= ' rel="'.$link['rel'].'"'; 1050 if(!empty($link['more'])) $ret .= ' '.$link['more']; 1051 $ret .= '>'; 1052 $ret .= $link['name']; 1053 $ret .= '</a>'; 1054 $ret .= $link['suf']; 1055 return $ret; 1056 } 1057 1058 /** 1059 * Renders internal and external media 1060 * 1061 * @author Andreas Gohr <andi@splitbrain.org> 1062 */ 1063 function _media ($src, $title=null, $align=null, $width=null, 1064 $height=null, $cache=null, $render = true) { 1065 1066 $ret = ''; 1067 1068 list($ext,$mime,$dl) = mimetype($src); 1069 if(substr($mime,0,5) == 'image'){ 1070 // first get the $title 1071 if (!is_null($title)) { 1072 $title = $this->_xmlEntities($title); 1073 }elseif($ext == 'jpg' || $ext == 'jpeg'){ 1074 //try to use the caption from IPTC/EXIF 1075 require_once(DOKU_INC.'inc/JpegMeta.php'); 1076 $jpeg =new JpegMeta(mediaFN($src)); 1077 if($jpeg !== false) $cap = $jpeg->getTitle(); 1078 if($cap){ 1079 $title = $this->_xmlEntities($cap); 1080 } 1081 } 1082 if (!$render) { 1083 // if the picture is not supposed to be rendered 1084 // return the title of the picture 1085 if (!$title) { 1086 // just show the sourcename 1087 $title = $this->_xmlEntities(utf8_basename(noNS($src))); 1088 } 1089 return $title; 1090 } 1091 //add image tag 1092 $ret .= '<img src="'.ml($src,array('w'=>$width,'h'=>$height,'cache'=>$cache)).'"'; 1093 $ret .= ' class="media'.$align.'"'; 1094 1095 if ($title) { 1096 $ret .= ' title="' . $title . '"'; 1097 $ret .= ' alt="' . $title .'"'; 1098 }else{ 1099 $ret .= ' alt=""'; 1100 } 1101 1102 if ( !is_null($width) ) 1103 $ret .= ' width="'.$this->_xmlEntities($width).'"'; 1104 1105 if ( !is_null($height) ) 1106 $ret .= ' height="'.$this->_xmlEntities($height).'"'; 1107 1108 $ret .= ' />'; 1109 1110 }elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')){ 1111 // first get the $title 1112 $title = !is_null($title) ? $this->_xmlEntities($title) : false; 1113 if (!$render) { 1114 // if the file is not supposed to be rendered 1115 // return the title of the file (just the sourcename if there is no title) 1116 return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src))); 1117 } 1118 1119 $att = array(); 1120 $att['class'] = "media$align"; 1121 if ($title) { 1122 $att['title'] = $title; 1123 } 1124 1125 if (media_supportedav($mime, 'video')) { 1126 //add video 1127 $ret .= $this->_video($src, $width, $height, $att); 1128 } 1129 if (media_supportedav($mime, 'audio')) { 1130 //add audio 1131 $ret .= $this->_audio($src, $att); 1132 } 1133 1134 }elseif($mime == 'application/x-shockwave-flash'){ 1135 if (!$render) { 1136 // if the flash is not supposed to be rendered 1137 // return the title of the flash 1138 if (!$title) { 1139 // just show the sourcename 1140 $title = utf8_basename(noNS($src)); 1141 } 1142 return $this->_xmlEntities($title); 1143 } 1144 1145 $att = array(); 1146 $att['class'] = "media$align"; 1147 if($align == 'right') $att['align'] = 'right'; 1148 if($align == 'left') $att['align'] = 'left'; 1149 $ret .= html_flashobject(ml($src,array('cache'=>$cache),true,'&'),$width,$height, 1150 array('quality' => 'high'), 1151 null, 1152 $att, 1153 $this->_xmlEntities($title)); 1154 }elseif($title){ 1155 // well at least we have a title to display 1156 $ret .= $this->_xmlEntities($title); 1157 }else{ 1158 // just show the sourcename 1159 $ret .= $this->_xmlEntities(utf8_basename(noNS($src))); 1160 } 1161 1162 return $ret; 1163 } 1164 1165 function _xmlEntities($string) { 1166 return htmlspecialchars($string,ENT_QUOTES,'UTF-8'); 1167 } 1168 1169 /** 1170 * Creates a linkid from a headline 1171 * 1172 * @param string $title The headline title 1173 * @param boolean $create Create a new unique ID? 1174 * @author Andreas Gohr <andi@splitbrain.org> 1175 */ 1176 function _headerToLink($title,$create=false) { 1177 if($create){ 1178 return sectionID($title,$this->headers); 1179 }else{ 1180 $check = false; 1181 return sectionID($title,$check); 1182 } 1183 } 1184 1185 /** 1186 * Construct a title and handle images in titles 1187 * 1188 * @author Harry Fuecks <hfuecks@gmail.com> 1189 */ 1190 function _getLinkTitle($title, $default, & $isImage, $id=null, $linktype='content') { 1191 global $conf; 1192 1193 $isImage = false; 1194 if ( is_array($title) ) { 1195 $isImage = true; 1196 return $this->_imageTitle($title); 1197 } elseif ( is_null($title) || trim($title)=='') { 1198 if (useHeading($linktype) && $id) { 1199 $heading = p_get_first_heading($id); 1200 if ($heading) { 1201 return $this->_xmlEntities($heading); 1202 } 1203 } 1204 return $this->_xmlEntities($default); 1205 } else { 1206 return $this->_xmlEntities($title); 1207 } 1208 } 1209 1210 /** 1211 * Returns an HTML code for images used in link titles 1212 * 1213 * @todo Resolve namespace on internal images 1214 * @author Andreas Gohr <andi@splitbrain.org> 1215 */ 1216 function _imageTitle($img) { 1217 global $ID; 1218 1219 // some fixes on $img['src'] 1220 // see internalmedia() and externalmedia() 1221 list($img['src'],$hash) = explode('#',$img['src'],2); 1222 if ($img['type'] == 'internalmedia') { 1223 resolve_mediaid(getNS($ID),$img['src'],$exists); 1224 } 1225 1226 return $this->_media($img['src'], 1227 $img['title'], 1228 $img['align'], 1229 $img['width'], 1230 $img['height'], 1231 $img['cache']); 1232 } 1233 1234 /** 1235 * _getMediaLinkConf is a helperfunction to internalmedia() and externalmedia() 1236 * which returns a basic link to a media. 1237 * 1238 * @author Pierre Spring <pierre.spring@liip.ch> 1239 * @param string $src 1240 * @param string $title 1241 * @param string $align 1242 * @param string $width 1243 * @param string $height 1244 * @param string $cache 1245 * @param string $render 1246 * @access protected 1247 * @return array 1248 */ 1249 function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) { 1250 global $conf; 1251 1252 $link = array(); 1253 $link['class'] = 'media'; 1254 $link['style'] = ''; 1255 $link['pre'] = ''; 1256 $link['suf'] = ''; 1257 $link['more'] = ''; 1258 $link['target'] = $conf['target']['media']; 1259 $link['title'] = $this->_xmlEntities($src); 1260 $link['name'] = $this->_media($src, $title, $align, $width, $height, $cache, $render); 1261 1262 return $link; 1263 } 1264 1265 1266 /** 1267 * Embed video(s) in HTML 1268 * 1269 * @author Anika Henke <anika@selfthinker.org> 1270 * 1271 * @param string $src - ID of video to embed 1272 * @param int $width - width of the video in pixels 1273 * @param int $height - height of the video in pixels 1274 * @param array $atts - additional attributes for the <video> tag 1275 * @return string 1276 */ 1277 function _video($src,$width,$height,$atts=null){ 1278 // prepare width and height 1279 if(is_null($atts)) $atts = array(); 1280 $atts['width'] = (int) $width; 1281 $atts['height'] = (int) $height; 1282 if(!$atts['width']) $atts['width'] = 320; 1283 if(!$atts['height']) $atts['height'] = 240; 1284 1285 // prepare alternative formats 1286 $extensions = array('webm', 'ogv', 'mp4'); 1287 $alternatives = media_alternativefiles($src, $extensions); 1288 $poster = media_alternativefiles($src, array('jpg', 'png'), true); 1289 $posterUrl = ''; 1290 if (!empty($poster)) { 1291 $posterUrl = ml(reset($poster),array('cache'=>$cache),true,'&'); 1292 } 1293 1294 $out = ''; 1295 // open video tag 1296 $out .= '<video '.buildAttributes($atts).' controls="controls"'; 1297 if ($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"'; 1298 $out .= '>'.NL; 1299 $fallback = ''; 1300 1301 // output source for each alternative video format 1302 foreach($alternatives as $mime => $file) { 1303 $url = ml($file,array('cache'=>$cache),true,'&'); 1304 $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file))); 1305 1306 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1307 // alternative content (just a link to the file) 1308 $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true); 1309 } 1310 1311 // finish 1312 $out .= $fallback; 1313 $out .= '</video>'.NL; 1314 return $out; 1315 } 1316 1317 /** 1318 * Embed audio in HTML 1319 * 1320 * @author Anika Henke <anika@selfthinker.org> 1321 * 1322 * @param string $src - ID of audio to embed 1323 * @param array $atts - additional attributes for the <audio> tag 1324 * @return string 1325 */ 1326 function _audio($src,$atts=null){ 1327 1328 // prepare alternative formats 1329 $extensions = array('ogg', 'mp3', 'wav'); 1330 $alternatives = media_alternativefiles($src, $extensions); 1331 1332 $out = ''; 1333 // open audio tag 1334 $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL; 1335 $fallback = ''; 1336 1337 // output source for each alternative audio format 1338 foreach($alternatives as $mime => $file) { 1339 $url = ml($file,array('cache'=>$cache),true,'&'); 1340 $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file))); 1341 1342 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1343 // alternative content (just a link to the file) 1344 $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true); 1345 } 1346 1347 // finish 1348 $out .= $fallback; 1349 $out .= '</audio>'.NL; 1350 return $out; 1351 } 1352 1353} 1354 1355//Setup VIM: ex: et ts=4 : 1356