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