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 $exists = null; 704 $url = $this->_resolveInterWiki($wikiName, $wikiUri, $exists); 705 706 if(!$isImage) { 707 $class = preg_replace('/[^_\-a-z0-9]+/i', '_', $wikiName); 708 $link['class'] = "interwiki iw_$class"; 709 } else { 710 $link['class'] = 'media'; 711 } 712 713 //do we stay at the same server? Use local target 714 if(strpos($url, DOKU_URL) === 0 OR strpos($url, DOKU_BASE) === 0) { 715 $link['target'] = $conf['target']['wiki']; 716 } 717 if($exists !== null && !$isImage) { 718 if($exists) { 719 $link['class'] .= ' wikilink1'; 720 } else { 721 $link['class'] .= ' wikilink2'; 722 $link['rel'] = 'nofollow'; 723 } 724 } 725 726 $link['url'] = $url; 727 $link['title'] = htmlspecialchars($link['url']); 728 729 //output formatted 730 $this->doc .= $this->_formatLink($link); 731 } 732 733 /** 734 */ 735 function windowssharelink($url, $name = null) { 736 global $conf; 737 global $lang; 738 //simple setup 739 $link['target'] = $conf['target']['windows']; 740 $link['pre'] = ''; 741 $link['suf'] = ''; 742 $link['style'] = ''; 743 744 $link['name'] = $this->_getLinkTitle($name, $url, $isImage); 745 if ( !$isImage ) { 746 $link['class'] = 'windows'; 747 } else { 748 $link['class'] = 'media'; 749 } 750 751 $link['title'] = $this->_xmlEntities($url); 752 $url = str_replace('\\','/',$url); 753 $url = 'file:///'.$url; 754 $link['url'] = $url; 755 756 //output formatted 757 $this->doc .= $this->_formatLink($link); 758 } 759 760 function emaillink($address, $name = null) { 761 global $conf; 762 //simple setup 763 $link = array(); 764 $link['target'] = ''; 765 $link['pre'] = ''; 766 $link['suf'] = ''; 767 $link['style'] = ''; 768 $link['more'] = ''; 769 770 $name = $this->_getLinkTitle($name, '', $isImage); 771 if ( !$isImage ) { 772 $link['class']='mail'; 773 } else { 774 $link['class']='media'; 775 } 776 777 $address = $this->_xmlEntities($address); 778 $address = obfuscate($address); 779 $title = $address; 780 781 if(empty($name)){ 782 $name = $address; 783 } 784 785 if($conf['mailguard'] == 'visible') $address = rawurlencode($address); 786 787 $link['url'] = 'mailto:'.$address; 788 $link['name'] = $name; 789 $link['title'] = $title; 790 791 //output formatted 792 $this->doc .= $this->_formatLink($link); 793 } 794 795 function internalmedia ($src, $title=null, $align=null, $width=null, 796 $height=null, $cache=null, $linking=null, $return=NULL) { 797 global $ID; 798 list($src,$hash) = explode('#',$src,2); 799 resolve_mediaid(getNS($ID),$src, $exists); 800 801 $noLink = false; 802 $render = ($linking == 'linkonly') ? false : true; 803 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 804 805 list($ext,$mime,$dl) = mimetype($src,false); 806 if(substr($mime,0,5) == 'image' && $render){ 807 $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),($linking=='direct')); 808 }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){ 809 // don't link movies 810 $noLink = true; 811 }else{ 812 // add file icons 813 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext); 814 $link['class'] .= ' mediafile mf_'.$class; 815 $link['url'] = ml($src,array('id'=>$ID,'cache'=>$cache),true); 816 if ($exists) $link['title'] .= ' (' . filesize_h(filesize(mediaFN($src))).')'; 817 } 818 819 if($hash) $link['url'] .= '#'.$hash; 820 821 //markup non existing files 822 if (!$exists) { 823 $link['class'] .= ' wikilink2'; 824 } 825 826 //output formatted 827 if ($return) { 828 if ($linking == 'nolink' || $noLink) return $link['name']; 829 else return $this->_formatLink($link); 830 } else { 831 if ($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 832 else $this->doc .= $this->_formatLink($link); 833 } 834 } 835 836 function externalmedia ($src, $title=null, $align=null, $width=null, 837 $height=null, $cache=null, $linking=null) { 838 list($src,$hash) = explode('#',$src,2); 839 $noLink = false; 840 $render = ($linking == 'linkonly') ? false : true; 841 $link = $this->_getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render); 842 843 $link['url'] = ml($src,array('cache'=>$cache)); 844 845 list($ext,$mime,$dl) = mimetype($src,false); 846 if(substr($mime,0,5) == 'image' && $render){ 847 // link only jpeg images 848 // if ($ext != 'jpg' && $ext != 'jpeg') $noLink = true; 849 }elseif(($mime == 'application/x-shockwave-flash' || media_supportedav($mime)) && $render){ 850 // don't link movies 851 $noLink = true; 852 }else{ 853 // add file icons 854 $class = preg_replace('/[^_\-a-z0-9]+/i','_',$ext); 855 $link['class'] .= ' mediafile mf_'.$class; 856 } 857 858 if($hash) $link['url'] .= '#'.$hash; 859 860 //output formatted 861 if ($linking == 'nolink' || $noLink) $this->doc .= $link['name']; 862 else $this->doc .= $this->_formatLink($link); 863 } 864 865 /** 866 * Renders an RSS feed 867 * 868 * @author Andreas Gohr <andi@splitbrain.org> 869 */ 870 function rss ($url,$params){ 871 global $lang; 872 global $conf; 873 874 require_once(DOKU_INC.'inc/FeedParser.php'); 875 $feed = new FeedParser(); 876 $feed->set_feed_url($url); 877 878 //disable warning while fetching 879 if (!defined('DOKU_E_LEVEL')) { $elvl = error_reporting(E_ERROR); } 880 $rc = $feed->init(); 881 if (!defined('DOKU_E_LEVEL')) { error_reporting($elvl); } 882 883 //decide on start and end 884 if($params['reverse']){ 885 $mod = -1; 886 $start = $feed->get_item_quantity()-1; 887 $end = $start - ($params['max']); 888 $end = ($end < -1) ? -1 : $end; 889 }else{ 890 $mod = 1; 891 $start = 0; 892 $end = $feed->get_item_quantity(); 893 $end = ($end > $params['max']) ? $params['max'] : $end; 894 } 895 896 $this->doc .= '<ul class="rss">'; 897 if($rc){ 898 for ($x = $start; $x != $end; $x += $mod) { 899 $item = $feed->get_item($x); 900 $this->doc .= '<li><div class="li">'; 901 // support feeds without links 902 $lnkurl = $item->get_permalink(); 903 if($lnkurl){ 904 // title is escaped by SimplePie, we unescape here because it 905 // is escaped again in externallink() FS#1705 906 $this->externallink($item->get_permalink(), 907 html_entity_decode($item->get_title(), ENT_QUOTES, 'UTF-8')); 908 }else{ 909 $this->doc .= ' '.$item->get_title(); 910 } 911 if($params['author']){ 912 $author = $item->get_author(0); 913 if($author){ 914 $name = $author->get_name(); 915 if(!$name) $name = $author->get_email(); 916 if($name) $this->doc .= ' '.$lang['by'].' '.$name; 917 } 918 } 919 if($params['date']){ 920 $this->doc .= ' ('.$item->get_local_date($conf['dformat']).')'; 921 } 922 if($params['details']){ 923 $this->doc .= '<div class="detail">'; 924 if($conf['htmlok']){ 925 $this->doc .= $item->get_description(); 926 }else{ 927 $this->doc .= strip_tags($item->get_description()); 928 } 929 $this->doc .= '</div>'; 930 } 931 932 $this->doc .= '</div></li>'; 933 } 934 }else{ 935 $this->doc .= '<li><div class="li">'; 936 $this->doc .= '<em>'.$lang['rssfailed'].'</em>'; 937 $this->externallink($url); 938 if($conf['allowdebug']){ 939 $this->doc .= '<!--'.hsc($feed->error).'-->'; 940 } 941 $this->doc .= '</div></li>'; 942 } 943 $this->doc .= '</ul>'; 944 } 945 946 // $numrows not yet implemented 947 function table_open($maxcols = null, $numrows = null, $pos = null){ 948 global $lang; 949 // initialize the row counter used for classes 950 $this->_counter['row_counter'] = 0; 951 $class = 'table'; 952 if ($pos !== null) { 953 $class .= ' ' . $this->startSectionEdit($pos, 'table'); 954 } 955 $this->doc .= '<div class="' . $class . '"><table class="inline">' . 956 DOKU_LF; 957 } 958 959 function table_close($pos = null){ 960 $this->doc .= '</table></div>'.DOKU_LF; 961 if ($pos !== null) { 962 $this->finishSectionEdit($pos); 963 } 964 } 965 966 function tablerow_open(){ 967 // initialize the cell counter used for classes 968 $this->_counter['cell_counter'] = 0; 969 $class = 'row' . $this->_counter['row_counter']++; 970 $this->doc .= DOKU_TAB . '<tr class="'.$class.'">' . DOKU_LF . DOKU_TAB . DOKU_TAB; 971 } 972 973 function tablerow_close(){ 974 $this->doc .= DOKU_LF . DOKU_TAB . '</tr>' . DOKU_LF; 975 } 976 977 function tableheader_open($colspan = 1, $align = null, $rowspan = 1){ 978 $class = 'class="col' . $this->_counter['cell_counter']++; 979 if ( !is_null($align) ) { 980 $class .= ' '.$align.'align'; 981 } 982 $class .= '"'; 983 $this->doc .= '<th ' . $class; 984 if ( $colspan > 1 ) { 985 $this->_counter['cell_counter'] += $colspan-1; 986 $this->doc .= ' colspan="'.$colspan.'"'; 987 } 988 if ( $rowspan > 1 ) { 989 $this->doc .= ' rowspan="'.$rowspan.'"'; 990 } 991 $this->doc .= '>'; 992 } 993 994 function tableheader_close(){ 995 $this->doc .= '</th>'; 996 } 997 998 function tablecell_open($colspan = 1, $align = null, $rowspan = 1){ 999 $class = 'class="col' . $this->_counter['cell_counter']++; 1000 if ( !is_null($align) ) { 1001 $class .= ' '.$align.'align'; 1002 } 1003 $class .= '"'; 1004 $this->doc .= '<td '.$class; 1005 if ( $colspan > 1 ) { 1006 $this->_counter['cell_counter'] += $colspan-1; 1007 $this->doc .= ' colspan="'.$colspan.'"'; 1008 } 1009 if ( $rowspan > 1 ) { 1010 $this->doc .= ' rowspan="'.$rowspan.'"'; 1011 } 1012 $this->doc .= '>'; 1013 } 1014 1015 function tablecell_close(){ 1016 $this->doc .= '</td>'; 1017 } 1018 1019 //---------------------------------------------------------- 1020 // Utils 1021 1022 /** 1023 * Build a link 1024 * 1025 * Assembles all parts defined in $link returns HTML for the link 1026 * 1027 * @author Andreas Gohr <andi@splitbrain.org> 1028 */ 1029 function _formatLink($link){ 1030 //make sure the url is XHTML compliant (skip mailto) 1031 if(substr($link['url'],0,7) != 'mailto:'){ 1032 $link['url'] = str_replace('&','&',$link['url']); 1033 $link['url'] = str_replace('&amp;','&',$link['url']); 1034 } 1035 //remove double encodings in titles 1036 $link['title'] = str_replace('&amp;','&',$link['title']); 1037 1038 // be sure there are no bad chars in url or title 1039 // (we can't do this for name because it can contain an img tag) 1040 $link['url'] = strtr($link['url'],array('>'=>'%3E','<'=>'%3C','"'=>'%22')); 1041 $link['title'] = strtr($link['title'],array('>'=>'>','<'=>'<','"'=>'"')); 1042 1043 $ret = ''; 1044 $ret .= $link['pre']; 1045 $ret .= '<a href="'.$link['url'].'"'; 1046 if(!empty($link['class'])) $ret .= ' class="'.$link['class'].'"'; 1047 if(!empty($link['target'])) $ret .= ' target="'.$link['target'].'"'; 1048 if(!empty($link['title'])) $ret .= ' title="'.$link['title'].'"'; 1049 if(!empty($link['style'])) $ret .= ' style="'.$link['style'].'"'; 1050 if(!empty($link['rel'])) $ret .= ' rel="'.$link['rel'].'"'; 1051 if(!empty($link['more'])) $ret .= ' '.$link['more']; 1052 $ret .= '>'; 1053 $ret .= $link['name']; 1054 $ret .= '</a>'; 1055 $ret .= $link['suf']; 1056 return $ret; 1057 } 1058 1059 /** 1060 * Renders internal and external media 1061 * 1062 * @author Andreas Gohr <andi@splitbrain.org> 1063 */ 1064 function _media ($src, $title=null, $align=null, $width=null, 1065 $height=null, $cache=null, $render = true) { 1066 1067 $ret = ''; 1068 1069 list($ext,$mime,$dl) = mimetype($src); 1070 if(substr($mime,0,5) == 'image'){ 1071 // first get the $title 1072 if (!is_null($title)) { 1073 $title = $this->_xmlEntities($title); 1074 }elseif($ext == 'jpg' || $ext == 'jpeg'){ 1075 //try to use the caption from IPTC/EXIF 1076 require_once(DOKU_INC.'inc/JpegMeta.php'); 1077 $jpeg =new JpegMeta(mediaFN($src)); 1078 if($jpeg !== false) $cap = $jpeg->getTitle(); 1079 if($cap){ 1080 $title = $this->_xmlEntities($cap); 1081 } 1082 } 1083 if (!$render) { 1084 // if the picture is not supposed to be rendered 1085 // return the title of the picture 1086 if (!$title) { 1087 // just show the sourcename 1088 $title = $this->_xmlEntities(utf8_basename(noNS($src))); 1089 } 1090 return $title; 1091 } 1092 //add image tag 1093 $ret .= '<img src="'.ml($src,array('w'=>$width,'h'=>$height,'cache'=>$cache)).'"'; 1094 $ret .= ' class="media'.$align.'"'; 1095 1096 if ($title) { 1097 $ret .= ' title="' . $title . '"'; 1098 $ret .= ' alt="' . $title .'"'; 1099 }else{ 1100 $ret .= ' alt=""'; 1101 } 1102 1103 if ( !is_null($width) ) 1104 $ret .= ' width="'.$this->_xmlEntities($width).'"'; 1105 1106 if ( !is_null($height) ) 1107 $ret .= ' height="'.$this->_xmlEntities($height).'"'; 1108 1109 $ret .= ' />'; 1110 1111 }elseif(media_supportedav($mime, 'video') || media_supportedav($mime, 'audio')){ 1112 // first get the $title 1113 $title = !is_null($title) ? $this->_xmlEntities($title) : false; 1114 if (!$render) { 1115 // if the file is not supposed to be rendered 1116 // return the title of the file (just the sourcename if there is no title) 1117 return $title ? $title : $this->_xmlEntities(utf8_basename(noNS($src))); 1118 } 1119 1120 $att = array(); 1121 $att['class'] = "media$align"; 1122 if ($title) { 1123 $att['title'] = $title; 1124 } 1125 1126 if (media_supportedav($mime, 'video')) { 1127 //add video 1128 $ret .= $this->_video($src, $width, $height, $att); 1129 } 1130 if (media_supportedav($mime, 'audio')) { 1131 //add audio 1132 $ret .= $this->_audio($src, $att); 1133 } 1134 1135 }elseif($mime == 'application/x-shockwave-flash'){ 1136 if (!$render) { 1137 // if the flash is not supposed to be rendered 1138 // return the title of the flash 1139 if (!$title) { 1140 // just show the sourcename 1141 $title = utf8_basename(noNS($src)); 1142 } 1143 return $this->_xmlEntities($title); 1144 } 1145 1146 $att = array(); 1147 $att['class'] = "media$align"; 1148 if($align == 'right') $att['align'] = 'right'; 1149 if($align == 'left') $att['align'] = 'left'; 1150 $ret .= html_flashobject(ml($src,array('cache'=>$cache),true,'&'),$width,$height, 1151 array('quality' => 'high'), 1152 null, 1153 $att, 1154 $this->_xmlEntities($title)); 1155 }elseif($title){ 1156 // well at least we have a title to display 1157 $ret .= $this->_xmlEntities($title); 1158 }else{ 1159 // just show the sourcename 1160 $ret .= $this->_xmlEntities(utf8_basename(noNS($src))); 1161 } 1162 1163 return $ret; 1164 } 1165 1166 function _xmlEntities($string) { 1167 return htmlspecialchars($string,ENT_QUOTES,'UTF-8'); 1168 } 1169 1170 /** 1171 * Creates a linkid from a headline 1172 * 1173 * @param string $title The headline title 1174 * @param boolean $create Create a new unique ID? 1175 * @author Andreas Gohr <andi@splitbrain.org> 1176 */ 1177 function _headerToLink($title,$create=false) { 1178 if($create){ 1179 return sectionID($title,$this->headers); 1180 }else{ 1181 $check = false; 1182 return sectionID($title,$check); 1183 } 1184 } 1185 1186 /** 1187 * Construct a title and handle images in titles 1188 * 1189 * @author Harry Fuecks <hfuecks@gmail.com> 1190 */ 1191 function _getLinkTitle($title, $default, & $isImage, $id=null, $linktype='content') { 1192 global $conf; 1193 1194 $isImage = false; 1195 if ( is_array($title) ) { 1196 $isImage = true; 1197 return $this->_imageTitle($title); 1198 } elseif ( is_null($title) || trim($title)=='') { 1199 if (useHeading($linktype) && $id) { 1200 $heading = p_get_first_heading($id); 1201 if ($heading) { 1202 return $this->_xmlEntities($heading); 1203 } 1204 } 1205 return $this->_xmlEntities($default); 1206 } else { 1207 return $this->_xmlEntities($title); 1208 } 1209 } 1210 1211 /** 1212 * Returns an HTML code for images used in link titles 1213 * 1214 * @todo Resolve namespace on internal images 1215 * @author Andreas Gohr <andi@splitbrain.org> 1216 */ 1217 function _imageTitle($img) { 1218 global $ID; 1219 1220 // some fixes on $img['src'] 1221 // see internalmedia() and externalmedia() 1222 list($img['src'],$hash) = explode('#',$img['src'],2); 1223 if ($img['type'] == 'internalmedia') { 1224 resolve_mediaid(getNS($ID),$img['src'],$exists); 1225 } 1226 1227 return $this->_media($img['src'], 1228 $img['title'], 1229 $img['align'], 1230 $img['width'], 1231 $img['height'], 1232 $img['cache']); 1233 } 1234 1235 /** 1236 * _getMediaLinkConf is a helperfunction to internalmedia() and externalmedia() 1237 * which returns a basic link to a media. 1238 * 1239 * @author Pierre Spring <pierre.spring@liip.ch> 1240 * @param string $src 1241 * @param string $title 1242 * @param string $align 1243 * @param string $width 1244 * @param string $height 1245 * @param string $cache 1246 * @param string $render 1247 * @access protected 1248 * @return array 1249 */ 1250 function _getMediaLinkConf($src, $title, $align, $width, $height, $cache, $render) { 1251 global $conf; 1252 1253 $link = array(); 1254 $link['class'] = 'media'; 1255 $link['style'] = ''; 1256 $link['pre'] = ''; 1257 $link['suf'] = ''; 1258 $link['more'] = ''; 1259 $link['target'] = $conf['target']['media']; 1260 $link['title'] = $this->_xmlEntities($src); 1261 $link['name'] = $this->_media($src, $title, $align, $width, $height, $cache, $render); 1262 1263 return $link; 1264 } 1265 1266 1267 /** 1268 * Embed video(s) in HTML 1269 * 1270 * @author Anika Henke <anika@selfthinker.org> 1271 * 1272 * @param string $src - ID of video to embed 1273 * @param int $width - width of the video in pixels 1274 * @param int $height - height of the video in pixels 1275 * @param array $atts - additional attributes for the <video> tag 1276 * @return string 1277 */ 1278 function _video($src,$width,$height,$atts=null){ 1279 // prepare width and height 1280 if(is_null($atts)) $atts = array(); 1281 $atts['width'] = (int) $width; 1282 $atts['height'] = (int) $height; 1283 if(!$atts['width']) $atts['width'] = 320; 1284 if(!$atts['height']) $atts['height'] = 240; 1285 1286 // prepare alternative formats 1287 $extensions = array('webm', 'ogv', 'mp4'); 1288 $alternatives = media_alternativefiles($src, $extensions); 1289 $poster = media_alternativefiles($src, array('jpg', 'png'), true); 1290 $posterUrl = ''; 1291 if (!empty($poster)) { 1292 $posterUrl = ml(reset($poster),array('cache'=>$cache),true,'&'); 1293 } 1294 1295 $out = ''; 1296 // open video tag 1297 $out .= '<video '.buildAttributes($atts).' controls="controls"'; 1298 if ($posterUrl) $out .= ' poster="'.hsc($posterUrl).'"'; 1299 $out .= '>'.NL; 1300 $fallback = ''; 1301 1302 // output source for each alternative video format 1303 foreach($alternatives as $mime => $file) { 1304 $url = ml($file,array('cache'=>$cache),true,'&'); 1305 $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file))); 1306 1307 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1308 // alternative content (just a link to the file) 1309 $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true); 1310 } 1311 1312 // finish 1313 $out .= $fallback; 1314 $out .= '</video>'.NL; 1315 return $out; 1316 } 1317 1318 /** 1319 * Embed audio in HTML 1320 * 1321 * @author Anika Henke <anika@selfthinker.org> 1322 * 1323 * @param string $src - ID of audio to embed 1324 * @param array $atts - additional attributes for the <audio> tag 1325 * @return string 1326 */ 1327 function _audio($src,$atts=null){ 1328 1329 // prepare alternative formats 1330 $extensions = array('ogg', 'mp3', 'wav'); 1331 $alternatives = media_alternativefiles($src, $extensions); 1332 1333 $out = ''; 1334 // open audio tag 1335 $out .= '<audio '.buildAttributes($atts).' controls="controls">'.NL; 1336 $fallback = ''; 1337 1338 // output source for each alternative audio format 1339 foreach($alternatives as $mime => $file) { 1340 $url = ml($file,array('cache'=>$cache),true,'&'); 1341 $title = $atts['title'] ? $atts['title'] : $this->_xmlEntities(utf8_basename(noNS($file))); 1342 1343 $out .= '<source src="'.hsc($url).'" type="'.$mime.'" />'.NL; 1344 // alternative content (just a link to the file) 1345 $fallback .= $this->internalmedia($file, $title, NULL, NULL, NULL, $cache=NULL, $linking='linkonly', $return=true); 1346 } 1347 1348 // finish 1349 $out .= $fallback; 1350 $out .= '</audio>'.NL; 1351 return $out; 1352 } 1353 1354} 1355 1356//Setup VIM: ex: et ts=4 : 1357