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