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