1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Esther Brunner <wikidesign@gmail.com> 5 * @author Christopher Smith <chris@jalakai.co.uk> 6 * @author Gina Häußge, Michael Klier <dokuwiki@chimeric.de> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) die(); 11 12if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 13if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 14if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 15 16class helper_plugin_include extends DokuWiki_Plugin { // DokuWiki_Helper_Plugin 17 18 var $pages = array(); // filechain of included pages 19 var $page = array(); // associative array with data about the page to include 20 var $ins = array(); // instructions array 21 var $doc = ''; // the final output XHTML string 22 var $mode = 'section'; // inclusion mode: 'page' or 'section' 23 var $clevel = 0; // current section level 24 var $firstsec = 0; // show first section only 25 var $editbtn = 1; // show edit button 26 var $footer = 1; // show metaline below page 27 var $noheader = 0; // omit header 28 var $permalink = 0; // make first headline permalink to included page 29 var $redirect = 1; // redirect back to original page after an edit 30 var $header = array(); // included page / section header 31 var $renderer = NULL; // DokuWiki renderer object 32 33 var $INCLUDE_LIMIT = 12; 34 35 // private variables 36 var $_offset = NULL; 37 38 /** 39 * Constructor loads some config settings 40 */ 41 function helper_plugin_include() { 42 $this->firstsec = $this->getConf('firstseconly'); 43 $this->editbtn = $this->getConf('showeditbtn'); 44 $this->footer = $this->getConf('showfooter'); 45 $this->redirect = $this->getConf('doredirect'); 46 $this->noheader = 0; 47 $this->permalink = 0; 48 $this->header = array(); 49 } 50 51 function getInfo() { 52 return array( 53 'author' => 'Gina Häußge, Michael Klier, Esther Brunner', 54 'email' => 'dokuwiki@chimeric.de', 55 'date' => @file_get_contents(DOKU_PLUGIN . 'blog/VERSION'), 56 'name' => 'Include Plugin (helper class)', 57 'desc' => 'Functions to include another page in a wiki page', 58 'url' => 'http://wiki.splitbrain.org/plugin:include', 59 ); 60 } 61 62 function getMethods() { 63 $result = array(); 64 $result[] = array( 65 'name' => 'setPage', 66 'desc' => 'sets the page to include', 67 'params' => array("page attributes, 'id' required, 'section' for filtering" => 'array'), 68 'return' => array('success' => 'boolean'), 69 ); 70 $result[] = array( 71 'name' => 'setMode', 72 'desc' => 'sets inclusion mode: should indention be merged?', 73 'params' => array("'page' (original) or 'section' (merged indention)" => 'string'), 74 ); 75 $result[] = array( 76 'name' => 'setLevel', 77 'desc' => 'sets the indention for the current section level', 78 'params' => array('level: 0 to 5' => 'integer'), 79 'return' => array('success' => 'boolean'), 80 ); 81 $result[] = array( 82 'name' => 'setFlags', 83 'desc' => 'overrides standard values for showfooter and firstseconly settings', 84 'params' => array('flags' => 'array'), 85 ); 86 $result[] = array( 87 'name' => 'renderXHTML', 88 'desc' => 'renders the XHTML output of the included page', 89 'params' => array('DokuWiki renderer' => 'object'), 90 'return' => array('XHTML' => 'string'), 91 ); 92 return $result; 93 } 94 95 /** 96 * Sets the page to include if it is not already included (prevent recursion) 97 * and the current user is allowed to read it 98 */ 99 function setPage($page) { 100 global $ID; 101 102 $id = $page['id']; 103 $fullid = $id.'#'.$page['section']; 104 105 if (!$id) return false; // no page id given 106 if ($id == $ID) return false; // page can't include itself 107 108 // prevent include recursion 109 if ($this->_in_filechain($id,$page['section']) || (count($this->pages) >= $this->INCLUDE_LIMIT)) return false; 110 111 // we need to make sure 'perm', 'file' and 'exists' are set 112 if (!isset($page['perm'])) $page['perm'] = auth_quickaclcheck($page['id']); 113 if (!isset($page['file'])) $page['file'] = wikiFN($page['id']); 114 if (!isset($page['exists'])) $page['exists'] = @file_exists($page['file']); 115 116 // check permission 117 if ($page['perm'] < AUTH_READ) return false; 118 119 // add the page to the filechain 120 $this->page = $page; 121 return true; 122 } 123 124 function _push_page($id,$section) { 125 global $ID; 126 if (empty($this->pages)) array_push($this->pages, $ID.'#'); 127 array_push($this->pages, $id.'#'.$section); 128 } 129 130 function _pop_page() { 131 $page = array_pop($this->pages); 132 if (count($this->pages=1)) $this->pages = array(); 133 134 return $page; 135 } 136 137 function _in_filechain($id,$section) { 138 $pattern = $section ? "/^($id#$section|$id#)$/" : "/^$id#/"; 139 $match = preg_grep($pattern, $this->pages); 140 141 return (!empty($match)); 142 } 143 144 /** 145 * Sets the inclusion mode: 'page' or 'section' 146 */ 147 function setMode($mode) { 148 $this->mode = $mode; 149 } 150 151 /** 152 * Sets the right indention for a given section level 153 */ 154 function setLevel($level) { 155 if ((is_numeric($level)) && ($level >= 0) && ($level <= 5)) { 156 $this->clevel = $level; 157 return true; 158 } 159 return false; 160 } 161 162 /** 163 * Overrides standard values for showfooter and firstseconly settings 164 */ 165 function setFlags($flags) { 166 foreach ($flags as $flag) { 167 switch ($flag) { 168 case 'footer': 169 $this->footer = 1; 170 break; 171 case 'nofooter': 172 $this->footer = 0; 173 break; 174 case 'firstseconly': 175 case 'firstsectiononly': 176 $this->firstsec = 1; 177 break; 178 case 'fullpage': 179 $this->firstsec = 0; 180 break; 181 case 'noheader': 182 $this->noheader = 1; 183 break; 184 case 'editbtn': 185 case 'editbutton': 186 $this->editbtn = 1; 187 break; 188 case 'noeditbtn': 189 case 'noeditbutton': 190 $this->editbtn = 0; 191 break; 192 case 'permalink': 193 $this->permalink = 1; 194 break; 195 case 'nopermalink': 196 $this->permalink = 0; 197 break; 198 case 'redirect': 199 $this->redirect = 1; 200 break; 201 case 'noredirect': 202 $this->redirect = 0; 203 break; 204 } 205 } 206 } 207 208 /** 209 * Builds the XHTML to embed the page to include 210 */ 211 function renderXHTML(&$renderer, &$info) { 212 global $ID; 213 214 if (!$this->page['id']) return ''; // page must be set first 215 if (!$this->page['exists'] && ($this->page['perm'] < AUTH_CREATE)) return ''; 216 217 $this->_push_page($this->page['id'],$this->page['section']); 218 219 // prepare variables 220 $rdoc = $renderer->doc; 221 $doc = ''; 222 $this->renderer =& $renderer; 223 224 $page = $this->page; 225 $clevel = $this->clevel; 226 $mode = $this->mode; 227 228 // exchange page ID for included one 229 $backupID = $ID; // store the current ID 230 $ID = $this->page['id']; // change ID to the included page 231 232 // get instructions and render them on the fly 233 $this->ins = p_cached_instructions($this->page['file']); 234 235 // show only a given section? 236 if ($this->page['section'] && $this->page['exists']) $this->_getSection(); 237 238 // convert relative links 239 $this->_convertInstructions(); 240 241 $xhtml = p_render('xhtml', $this->ins, $info); 242 $ID = $backupID; // restore ID 243 244 $this->mode = $mode; 245 $this->clevel = $clevel; 246 $this->page = $page; 247 248 // render the included page 249 $content = '<div class="entry-content">'.DOKU_LF. 250 $this->_cleanXHTML($xhtml).DOKU_LF. 251 '</div><!-- .entry-content -->'.DOKU_LF; 252 253 // restore ID 254 $ID = $backupID; 255 256 // embed the included page 257 $class = ($this->page['draft'] ? 'include draft' : 'include'); 258 259 $doc .= DOKU_LF.'<!-- including '.$this->page['id'].' // '.$this->page['file'].' -->'.DOKU_LF; 260 $doc .= '<div class="'.$class.' hentry"'.$this->_showTagLogos().'>'.DOKU_LF; 261 if (!$this->header && $this->clevel && ($this->mode == 'section')) 262 $doc .= '<div class="level'.$this->clevel.'">'.DOKU_LF; 263 264 if ((@file_exists(DOKU_PLUGIN.'editsections/action.php')) 265 && (!plugin_isdisabled('editsections'))) { // for Edit Section Reorganizer Plugin 266 $doc .= $this->_editButton().$content; 267 } else { 268 $doc .= $content.$this->_editButton(); 269 } 270 271 // output meta line (if wanted) and remove page from filechain 272 $doc .= $this->_footer($this->page); 273 274 if (!$this->header && $this->clevel && ($this->mode == 'section')) 275 $doc .= '</div>'.DOKU_LF; // class="level?" 276 $doc .= '</div>'.DOKU_LF; // class="include hentry" 277 $doc .= DOKU_LF.'<!-- /including '.$this->page['id'].' -->'.DOKU_LF; 278 279 // reset defaults 280 $this->helper_plugin_include(); 281 $this->_pop_page(); 282 283 // return XHTML 284 $renderer->doc = $rdoc.$doc; 285 return $doc; 286 } 287 288 /* ---------- Private Methods ---------- */ 289 290 /** 291 * Get a section including its subsections 292 */ 293 function _getSection() { 294 foreach ($this->ins as $ins) { 295 if ($ins[0] == 'header') { 296 297 // found the right header 298 if (cleanID($ins[1][0]) == $this->page['section']) { 299 $level = $ins[1][1]; 300 $i[] = $ins; 301 302 // next header of the same or higher level -> exit 303 } elseif ($ins[1][1] <= $level) { 304 $this->ins = $i; 305 return true; 306 } elseif (isset($level)) { 307 $i[] = $ins; 308 } 309 310 // add instructions from our section 311 } elseif (isset($level)) { 312 $i[] = $ins; 313 } 314 } 315 $this->ins = $i; 316 return true; 317 } 318 319 /** 320 * Corrects relative internal links and media and 321 * converts headers of included pages to subheaders of the current page 322 */ 323 function _convertInstructions() { 324 global $ID; 325 326 if (!$this->page['exists']) return false; 327 328 // check if included page is in same namespace 329 $ns = getNS($this->page['id']); 330 $convert = (getNS($ID) == $ns ? false : true); 331 332 $n = count($this->ins); 333 for ($i = 0; $i < $n; $i++) { 334 $current = $this->ins[$i][0]; 335 336 // convert internal links and media from relative to absolute 337 if ($convert && (substr($current, 0, 8) == 'internal')) { 338 $this->ins[$i][1][0] = $this->_convertInternalLink($this->ins[$i][1][0], $ns); 339 340 // set header level to current section level + header level 341 } elseif ($current == 'header') { 342 $this->_convertHeader($i); 343 344 // the same for sections 345 } elseif (($current == 'section_open') && ($this->mode == 'section')) { 346 $this->ins[$i][1][0] = $this->_convertSectionLevel($this->ins[$i][1][0]); 347 348 // show only the first section? 349 } elseif ($this->firstsec && ($current == 'section_close') 350 && ($this->ins[$i-1][0] != 'section_open')) { 351 $this->_readMore($i); 352 return true; 353 } 354 } 355 $this->_finishConvert(); 356 return true; 357 } 358 359 /** 360 * Convert relative internal links and media 361 * 362 * @param integer $i: counter for current instruction 363 * @param string $ns: namespace of included page 364 * @return string $link: converted, now absolute link 365 */ 366 function _convertInternalLink($link, $ns) { 367 368 // relative subnamespace 369 if ($link{0} == '.') { 370 if ($link{1} == '.') return getNS($ns).':'.substr($link, 2); // parent namespace 371 else return $ns.':'.substr($link, 1); // current namespace 372 373 // relative link 374 } elseif (strpos($link, ':') === false) { 375 return $ns.':'.$link; 376 377 // absolute link - don't change 378 } else { 379 return $link; 380 } 381 } 382 383 /** 384 * Convert header level and add header to TOC 385 * 386 * @param integer $i: counter for current instruction 387 * @return boolean true 388 */ 389 function _convertHeader($i) { 390 global $conf; 391 392 $text = $this->ins[$i][1][0]; 393 $hid = $this->renderer->_headerToLink($text, 'true'); 394 if (empty($this->header)) { 395 $this->_offset = $this->clevel - $this->ins[$i][1][1] + 1; 396 $level = $this->_convertSectionLevel(1); 397 $this->header = array('hid' => $hid, 'title' => hsc($text), 'level' => $level); 398 if ($this->noheader) { 399 unset($this->ins[$i]); 400 return true; 401 } else if ($this->permalink){ 402 $this->ins[$i] = $this->_permalinkHeader($text, $level, $this->ins[$i][1][2]); 403 } 404 } else { 405 $level = $this->_convertSectionLevel($this->ins[$i][1][1]); 406 } 407 if ($this->mode == 'section') { 408 if (is_array($this->ins[$i][1][1])) { // permalink header 409 $this->ins[$i][1][1][1] = $level; 410 } else { // normal header 411 $this->ins[$i][1][1] = $level; 412 } 413 } 414 415 // add TOC item 416 if (($level >= $conf['toptoclevel']) && ($level <= $conf['maxtoclevel'])) { 417 $this->renderer->toc[] = array( 418 'hid' => $hid, 419 'title' => $text, 420 'type' => 'ul', 421 'level' => $level - $conf['toptoclevel'] + 1 422 ); 423 } 424 return true; 425 } 426 427 /** 428 * Create instruction item for a permalink header 429 * 430 * @param string $text: Headline text 431 * @param integer $level: Headline level 432 * @param integer $pos: I wish I knew what this is for... 433 * 434 * @author Gina Haeussge <osd@foosel.net> 435 */ 436 function _permalinkHeader($text, $level, $pos) { 437 $newIns = array( 438 'plugin', 439 array( 440 'include_header', 441 array( 442 $text, 443 $level 444 ), 445 ), 446 $pos 447 ); 448 449 return $newIns; 450 } 451 452 /** 453 * Convert the level of headers and sections 454 * 455 * @param integer $in: current level 456 * @return integer $out: converted level 457 */ 458 function _convertSectionLevel($in) { 459 $out = $in + $this->_offset; 460 if ($out >= 5) return 5; 461 if ($out <= $this->clevel + 1) return $this->clevel + 1; 462 return $out; 463 } 464 465 /** 466 * Adds a read more... link at the bottom of the first section 467 * 468 * @param integer $i: counter for current instruction 469 * @return boolean true 470 */ 471 function _readMore($i) { 472 $more = ((is_array($this->ins[$i+1])) && ($this->ins[$i+1][0] != 'document_end')); 473 474 if ($this->ins[0][0] == 'document_start') $this->ins = array_slice($this->ins, 1, $i); 475 else $this->ins = array_slice($this->ins, 0, $i); 476 477 if ($more) { 478 array_unshift($this->ins, array('document_start', array(), 0)); 479 $last = array_pop($this->ins); 480 $this->ins[] = array('p_open', array(), $last[2]); 481 $this->ins[] = array('internallink',array($this->page['id'], $this->getLang('readmore')),$last[2]); 482 $this->ins[] = array('p_close', array(), $last[2]); 483 $this->ins[] = $last; 484 $this->ins[] = array('document_end', array(), $last[2]); 485 } else { 486 $this->_finishConvert(); 487 } 488 return true; 489 } 490 491 /** 492 * Adds 'document_start' and 'document_end' instructions if not already there 493 */ 494 function _finishConvert() { 495 if ($this->ins[0][0] != 'document_start') 496 array_unshift($this->ins, array('document_start', array(), 0)); 497 $c = count($this->ins) - 1; 498 if ($this->ins[$c][0] != 'document_end') 499 $this->ins[] = array('document_end', array(), 0); 500 } 501 502 /** 503 * Remove TOC, section edit buttons and tags 504 */ 505 function _cleanXHTML($xhtml) { 506 $replace = array( 507 '!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove toc 508 '#<!-- SECTION "(.*?)" \[(\d+-\d*)\] -->#e' => '', // remove section edit buttons 509 '!<div class="tags">.*?(</div>)!s' => '', // remove category tags 510 ); 511 if ($this->clevel) 512 $replace['#<div class="footnotes">#s'] = '<div class="footnotes level'.$this->clevel.'">'; 513 $xhtml = preg_replace(array_keys($replace), array_values($replace), $xhtml); 514 return $xhtml; 515 } 516 517 /** 518 * Optionally display logo for the first tag found in the included page 519 */ 520 function _showTagLogos() { 521 if ((!$this->getConf('showtaglogos')) 522 || (plugin_isdisabled('tag')) 523 || (!$taghelper =& plugin_load('helper', 'tag'))) 524 return ''; 525 526 $subject = p_get_metadata($this->page['id'], 'subject'); 527 if (is_array($subject)) $tag = $subject[0]; 528 else list($tag, $rest) = explode(' ', $subject, 2); 529 $title = str_replace('_', ' ', noNS($tag)); 530 resolve_pageid($taghelper->namespace, $tag, $exists); // resolve shortcuts 531 532 $logosrc = mediaFN($logoID); 533 $types = array('.png', '.jpg', '.gif'); // auto-detect filetype 534 foreach ($types as $type) { 535 if (!@file_exists($logosrc.$type)) continue; 536 $logoID = $tag.$type; 537 $logosrc .= $type; 538 list($w, $h, $t, $a) = getimagesize($logosrc); 539 return ' style="min-height: '.$h.'px">'. 540 '<img class="mediaright" src="'.ml($logoID).'" alt="'.$title.'"/'; 541 } 542 return ''; 543 } 544 545 /** 546 * Display an edit button for the included page 547 */ 548 function _editButton() { 549 global $ID; 550 if ($this->page['exists']) { 551 if (($this->page['perm'] >= AUTH_EDIT) && (is_writable($this->page['file']))) 552 $action = 'edit'; 553 else return ''; 554 } elseif ($this->page['perm'] >= AUTH_CREATE) { 555 $action = 'create'; 556 } 557 if ($this->editbtn) { 558 $params = array('do' => 'edit'); 559 if ($this->redirect) 560 $params['redirect_id'] = $ID; 561 return '<div class="secedit">'.DOKU_LF.DOKU_TAB. 562 html_btn($action, $this->page['id'], '', $params, 'post').DOKU_LF. 563 '</div>'.DOKU_LF; 564 } else { 565 return ''; 566 } 567 } 568 569 /** 570 * Returns the meta line below the included page 571 */ 572 function _footer($page) { 573 global $conf, $ID; 574 575 if (!$this->footer) return ''; // '<div class="inclmeta"> </div>'.DOKU_LF; 576 577 $id = $page['id']; 578 $meta = p_get_metadata($id); 579 $ret = array(); 580 581 // permalink 582 if ($this->getConf('showlink')) { 583 $title = ($page['title'] ? $page['title'] : $meta['title']); 584 if (!$title) $title = str_replace('_', ' ', noNS($id)); 585 $class = ($page['exists'] ? 'wikilink1' : 'wikilink2'); 586 $link = array( 587 'url' => wl($id), 588 'title' => $id, 589 'name' => hsc($title), 590 'target' => $conf['target']['wiki'], 591 'class' => $class.' permalink', 592 'more' => 'rel="bookmark"', 593 ); 594 $ret[] = $this->renderer->_formatLink($link); 595 } 596 597 // date 598 if ($this->getConf('showdate')) { 599 $date = ($page['date'] ? $page['date'] : $meta['date']['created']); 600 if ($date) 601 $ret[] = '<abbr class="published" title="'.strftime('%Y-%m-%dT%H:%M:%SZ', $date).'">'. 602 strftime($conf['dformat'], $date). 603 '</abbr>'; 604 } 605 606 // author 607 if ($this->getConf('showuser')) { 608 $author = ($page['user'] ? $page['user'] : $meta['creator']); 609 if ($author) { 610 $userpage = cleanID($this->getConf('usernamespace').':'.$author); 611 resolve_pageid(getNS($ID), $userpage, $exists); 612 $class = ($exists ? 'wikilink1' : 'wikilink2'); 613 $link = array( 614 'url' => wl($userpage), 615 'title' => $userpage, 616 'name' => hsc($author), 617 'target' => $conf['target']['wiki'], 618 'class' => $class.' url fn', 619 'pre' => '<span class="vcard author">', 620 'suf' => '</span>', 621 ); 622 $ret[] = $this->renderer->_formatLink($link); 623 } 624 } 625 626 // comments - let Discussion Plugin do the work for us 627 if (!$page['section'] && $this->getConf('showcomments') 628 && (!plugin_isdisabled('discussion')) 629 && ($discussion =& plugin_load('helper', 'discussion'))) { 630 $disc = $discussion->td($id); 631 if ($disc) $ret[] = '<span class="comment">'.$disc.'</span>'; 632 } 633 634 // linkbacks - let Linkback Plugin do the work for us 635 if (!$page['section'] && $this->getConf('showlinkbacks') 636 && (!plugin_isdisabled('linkback')) 637 && ($linkback =& plugin_load('helper', 'linkback'))) { 638 $link = $linkback->td($id); 639 if ($link) $ret[] = '<span class="linkback">'.$link.'</span>'; 640 } 641 642 $ret = implode(DOKU_LF.DOKU_TAB.'· ', $ret); 643 644 // tags - let Tag Plugin do the work for us 645 if (!$page['section'] && $this->getConf('showtags') 646 && (!plugin_isdisabled('tag')) 647 && ($tag =& plugin_load('helper', 'tag'))) { 648 $page['tags'] = '<div class="tags"><span>'.DOKU_LF. 649 DOKU_TAB.$tag->td($id).DOKU_LF. 650 DOKU_TAB.'</span></div>'.DOKU_LF; 651 $ret = $page['tags'].DOKU_TAB.$ret; 652 } 653 654 if (!$ret) $ret = ' '; 655 $class = 'inclmeta'; 656 if ($this->header && $this->clevel && ($this->mode == 'section')) 657 $class .= ' level'.$this->clevel; 658 return '<div class="'.$class.'">'.DOKU_LF.DOKU_TAB.$ret.DOKU_LF.'</div>'.DOKU_LF; 659 } 660 661 /** 662 * Builds the ODT to embed the page to include 663 */ 664 function renderODT(&$renderer) { 665 global $ID; 666 667 if (!$this->page['id']) return ''; // page must be set first 668 if (!$this->page['exists'] && ($this->page['perm'] < AUTH_CREATE)) return ''; 669 670 // prepare variable 671 $this->renderer =& $renderer; 672 673 // get instructions and render them on the fly 674 $this->ins = p_cached_instructions($this->page['file']); 675 676 // show only a given section? 677 if ($this->page['section'] && $this->page['exists']) $this->_getSection(); 678 679 // convert relative links 680 $this->_convertInstructions(); 681 682 // render the included page 683 $backupID = $ID; // store the current ID 684 $ID = $this->page['id']; // change ID to the included page 685 // remove document_start and document_end to avoid zipping 686 $this->ins = array_slice($this->ins, 1, -1); 687 p_render('odt', $this->ins, $info); 688 $ID = $backupID; // restore ID 689 // reset defaults 690 $this->helper_plugin_include(); 691 } 692} 693//vim:ts=4:sw=4:et:enc=utf-8: 694