1<?php 2/** 3 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 4 * @author Esther Brunner <wikidesign@gmail.com> 5 */ 6 7// must be run within Dokuwiki 8if (!defined('DOKU_INC')) die(); 9 10if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 11if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC.'lib/plugins/'); 13 14class helper_plugin_include extends DokuWiki_Plugin { // DokuWiki_Helper_Plugin 15 16 var $pages = array(); // filechain of included pages 17 var $page = array(); // associative array with data about the page to include 18 var $ins = array(); // instructions array 19 var $doc = ''; // the final output XHTML string 20 var $mode = 'section'; // inclusion mode: 'page' or 'section' 21 var $clevel = 0; // current section level 22 var $firstsec = 0; // show first section only 23 var $footer = 1; // show metaline below page 24 var $noheader = 0; // omit header 25 var $header = array(); // included page / section header 26 var $renderer = NULL; // DokuWiki renderer object 27 28 // private variables 29 var $_offset = NULL; 30 31 /** 32 * Constructor loads some config settings 33 */ 34 function helper_plugin_include(){ 35 $this->firstsec = $this->getConf('firstseconly'); 36 $this->footer = $this->getConf('showfooter'); 37 } 38 39 function getInfo(){ 40 return array( 41 'author' => 'Esther Brunner', 42 'email' => 'wikidesign@gmail.com', 43 'date' => '2007-08-09', 44 'name' => 'Include Plugin (helper class)', 45 'desc' => 'Functions to include another page in a wiki page', 46 'url' => 'http://www.wikidesign/en/plugin/include/start', 47 ); 48 } 49 50 function getMethods(){ 51 $result = array(); 52 $result[] = array( 53 'name' => 'setPage', 54 'desc' => 'sets the page to include', 55 'params' => array("page attributes, 'id' required, 'section' for filtering" => 'array'), 56 'return' => array('success' => 'boolean'), 57 ); 58 $result[] = array( 59 'name' => 'setMode', 60 'desc' => 'sets inclusion mode: should indention be merged?', 61 'params' => array("'page' (original) or 'section' (merged indention)" => 'string'), 62 ); 63 $result[] = array( 64 'name' => 'setLevel', 65 'desc' => 'sets the indention for the current section level', 66 'params' => array('level: 0 to 5' => 'integer'), 67 'return' => array('success' => 'boolean'), 68 ); 69 $result[] = array( 70 'name' => 'setFlags', 71 'desc' => 'overrides standard values for showfooter and firstseconly settings', 72 'params' => array('flags' => 'array'), 73 ); 74 $result[] = array( 75 'name' => 'renderXHTML', 76 'desc' => 'renders the XHTML output of the included page', 77 'params' => array('DokuWiki renderer' => 'object'), 78 'return' => array('XHTML' => 'string'), 79 ); 80 return $result; 81 } 82 83 /** 84 * Sets the page to include if it is not already included (prevent recursion) 85 * and the current user is allowed to read it 86 */ 87 function setPage($page){ 88 global $ID; 89 90 $id = $page['id']; 91 $fullid = $id.'#'.$page['section']; 92 93 if (!$id) return false; // no page id given 94 if ($id == $ID) return false; // page can't include itself 95 96 // prevent include recursion 97 if ((isset($this->pages[$id.'#'])) || (isset($this->pages[$fullid]))) return false; 98 99 // we need to make sure 'perm', 'file' and 'exists' are set 100 if (!isset($page['perm'])) $page['perm'] = auth_quickaclcheck($page['id']); 101 if (!isset($page['file'])) $page['file'] = wikiFN($page['id']); 102 if (!isset($page['exists'])) $page['exists'] = @file_exists($page['file']); 103 104 // check permission 105 if ($this->page['perm'] < AUTH_READ) return false; 106 107 // add the page to the filechain 108 $this->pages[$fullid] = $page; 109 $this->page =& $this->pages[$fullid]; 110 return true; 111 } 112 113 /** 114 * Sets the inclusion mode 115 */ 116 function setMode($mode){ 117 $this->mode = $mode; 118 } 119 120 /** 121 * Sets the right indention for a given section level 122 */ 123 function setLevel($level){ 124 if ((is_numeric($level)) && ($level >= 0) && ($level <= 5)){ 125 $this->clevel = $level; 126 return true; 127 } 128 return false; 129 } 130 131 /** 132 * Overrides standard values for showfooter and firstseconly settings 133 */ 134 function setFlags($flags){ 135 foreach ($flags as $flag){ 136 switch ($flag){ 137 case 'footer': 138 $this->footer = 1; 139 break; 140 case 'nofooter': 141 $this->footer = 0; 142 break; 143 case 'firstseconly': 144 $this->firstsec = 1; 145 break; 146 case 'fullpage': 147 $this->firstsec = 0; 148 break; 149 case 'noheader': 150 $this->noheader = 1; 151 break; 152 } 153 } 154 } 155 156 /** 157 * Builds the XHTML to embed the page to include 158 */ 159 function renderXHTML(&$renderer){ 160 if (!$this->page['id']) return ''; // page must be set first 161 if (!$this->page['exists'] && ($this->page['perm'] < AUTH_CREATE)) return ''; 162 163 // prepare variables 164 $this->doc = ''; 165 $this->renderer =& $renderer; 166 167 // get instructions and render them on the fly 168 $this->ins = p_cached_instructions($this->page['file']); 169 170 // show only a given section? 171 if ($this->page['section'] && $this->page['exists']) $this->_getSection(); 172 173 // convert relative links 174 $this->_convertInstructions(); 175 176 // render the included page 177 $content = '<div class="entry-content">'.DOKU_LF. 178 $this->_cleanXHTML(p_render('xhtml', $this->ins, $info)).DOKU_LF. 179 '</div>'.DOKU_LF; 180 181 // embed the included page 182 $class = ($this->page['draft'] ? 'include draft' : 'include'); 183 $renderer->doc .= '<div class="'.$class.' hentry"'.$this->_showTagLogos().'>'.DOKU_LF; 184 if (!$this->header && $this->clevel && ($this->mode == 'section')) 185 $renderer->doc .= '<div class="level'.$this->clevel.'">'.DOKU_LF; 186 if ((@file_exists(DOKU_PLUGIN.'editsections/action.php')) 187 && (!plugin_isdisabled('editsections'))){ // for Edit Section Reorganizer Plugin 188 $renderer->doc .= $this->_editButton().$content; 189 } else { 190 $renderer->doc .= $content.$this->_editButton(); 191 } 192 193 if (!$this->header && $this->clevel && ($this->mode == 'section')) 194 $renderer->doc .= '</div>'.DOKU_LF; // class="level?" 195 $renderer->doc .= '</div>'.DOKU_LF; // class="include hentry" 196 197 // output meta line (if wanted) and remove page from filechain 198 $renderer->doc .= $this->_footer(array_pop($this->pages)); 199 $this->helper_plugin_include(); 200 201 return $this->doc; 202 } 203 204/* ---------- Private Methods ---------- */ 205 206 /** 207 * Get a section including its subsections 208 */ 209 function _getSection(){ 210 foreach ($this->ins as $ins){ 211 if ($ins[0] == 'header'){ 212 213 // found the right header 214 if (cleanID($ins[1][0]) == $this->page['section']){ 215 $level = $ins[1][1]; 216 $i[] = $ins; 217 218 // next header of the same or higher level -> exit 219 } elseif ($ins[1][1] <= $level){ 220 $this->ins = $i; 221 return true; 222 } elseif (isset($level)){ 223 $i[] = $ins; 224 } 225 226 // add instructions from our section 227 } elseif (isset($level)){ 228 $i[] = $ins; 229 } 230 } 231 $this->ins = $i; 232 return true; 233 } 234 235 /** 236 * Corrects relative internal links and media and 237 * converts headers of included pages to subheaders of the current page 238 */ 239 function _convertInstructions(){ 240 global $ID; 241 global $conf; 242 243 $this->header = array(); 244 $offset = $this->clevel; 245 246 if (!$this->page['exists']) return false; 247 248 // check if included page is in same namespace 249 $inclNS = getNS($this->page['id']); 250 if (getNS($ID) == $inclNS) $convert = false; 251 else $convert = true; 252 253 $n = count($this->ins); 254 for ($i = 0; $i < $n; $i++){ 255 $current = $this->ins[$i][0]; 256 257 // convert internal links and media from relative to absolute 258 if ($convert && (substr($current, 0, 8) == 'internal')){ 259 $this->ins[$i][1][0] = $this->_convertInternalLinks($i, $inclNS); 260 261 // set header level to current section level + header level 262 } elseif ($current == 'header'){ 263 $this->_convertHeaders($i); 264 265 // the same for sections 266 } elseif ($current == 'section_open'){ 267 $this->ins[$i][1][0] = $this->_convertSectionLevel($this->ins[$i][1][0]); 268 269 // show only the first section? 270 } elseif ($this->firstsec && ($current == 'section_close') 271 && ($this->ins[$i-1][0] != 'section_open')){ 272 $this->_readMore($i); 273 return true; 274 } 275 } 276 $this->_finishConvert(); 277 return true; 278 } 279 280 /** 281 * Convert relative internal links and media 282 * 283 * @param integer $i: counter for current instruction 284 * @param string $ns: namespace of included page 285 * @return string $link: converted, now absolute link 286 */ 287 function _convertInternalLinks($i, $ns){ 288 289 // relative subnamespace 290 if ($this->ins[$i][1][0]{0} == '.'){ 291 292 // parent namespace 293 if ($this->ins[$i][1][0]{1} == '.') 294 return getNS($ns).':'.substr($this->ins[$i][1][0], 2); 295 296 // current namespace 297 else 298 return $ns.':'.substr($this->ins[$i][1][0], 1); 299 300 // relative link 301 } elseif (strpos($this->ins[$i][1][0], ':') === false){ 302 return $ns.':'.$this->ins[$i][1][0]; 303 } 304 } 305 306 /** 307 * Convert header level and add header to TOC 308 * 309 * @param integer $i: counter for current instruction 310 * @return boolean true 311 */ 312 function _convertHeaders($i){ 313 $text = $this->ins[$i][1][0]; 314 $hid = $this->renderer->_headerToLink($text, 'true'); 315 if (empty($this->header)){ 316 $this->_offset = $this->clevel - $this->ins[$i][1][1] + 1; 317 $level = $this->clevel + 1; 318 $this->header = array( 319 'hid' => $hid, 320 'title' => hsc($text), 321 'level' => $level 322 ); 323 if ($this->noheader){ 324 unset($this->ins[$i]); 325 return true; 326 } 327 } else { 328 $level = $this->_convertSectionLevel($this->ins[$i][1][1]); 329 } 330 331 $this->ins[$i][1][1] = $level; 332 333 // add TOC item 334 if (($level >= $conf['toptoclevel']) && ($level <= $conf['maxtoclevel'])){ 335 $this->renderer->toc[] = array( 336 'hid' => $hid, 337 'title' => $text, 338 'type' => 'ul', 339 'level' => $level - $conf['toptoclevel'] + 1 340 ); 341 } 342 return true; 343 } 344 345 /** 346 * Convert the level of headers and sections 347 * 348 * @param integer $in: current level 349 * @return integer $out: converted level 350 */ 351 function _convertSectionLevel($in){ 352 $out = $in + $this->_offset; 353 if ($out > 5) $out = 5; 354 return $out; 355 } 356 357 /** 358 * Adds a read more... link at the bottom of the first section 359 * 360 * @param integer $i: counter for current instruction 361 * @return boolean true 362 */ 363 function _readMore($i){ 364 $more = ((is_array($this->ins[$i+1])) && ($this->ins[$i+1][0] != 'document_end')); 365 366 if ($this->ins[0][0] == 'document_start') $this->ins = array_slice($this->ins, 1, $i); 367 else $this->ins = array_slice($this->ins, 0, $i); 368 369 if ($more){ 370 array_unshift($this->ins, array('document_start', array(), 0)); 371 $last = array_pop($this->ins); 372 $this->ins[] = array('p_open', array(), $last[2]); 373 $this->ins[] = array('internallink',array($this->page['id'], $this->getLang('readmore')),$last[2]); 374 $this->ins[] = array('p_close', array(), $last[2]); 375 $this->ins[] = $last; 376 $this->ins[] = array('document_end', array(), $last[2]); 377 } else { 378 $this->_finishConvert(); 379 } 380 return true; 381 } 382 383 /** 384 * Adds 'document_start' and 'document_end' instructions if not already there 385 */ 386 function _finishConvert(){ 387 if ($this->ins[0][0] != 'document_start'){ 388 array_unshift($this->ins, array('document_start', array(), 0)); 389 $this->ins[] = array('document_end', array(), 0); 390 } 391 } 392 393 /** 394 * Remove TOC, section edit buttons and tags 395 */ 396 function _cleanXHTML($xhtml){ 397 preg_match('!<div class="tags">.*?</div>!s', $xhtml, $match); 398 $this->page['tags'] = $match[0]; 399 $replace = array( 400 '!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove toc 401 '#<!-- SECTION "(.*?)" \[(\d+-\d*)\] -->#e' => '', // remove section edit buttons 402 '!<div class="tags">.*?(</div>)!s' => '', // remove category tags 403 ); 404 $xhtml = preg_replace(array_keys($replace), array_values($replace), $xhtml); 405 return $xhtml; 406 } 407 408 /** 409 * Optionally display logo for the first tag found in the included page 410 */ 411 function _showTagLogos(){ 412 if (!$this->getConf('showtaglogos')) return ''; 413 414 preg_match_all('/<a [^>]*title="(.*?)" rel="tag"[^>]*>([^<]*)</', $this->page['tags'], $tag); 415 $logoID = getNS($tag[1][0]).':'.$tag[2][0]; 416 $logosrc = mediaFN($logoID); 417 $types = array('.png', '.jpg', '.gif'); // auto-detect filetype 418 foreach ($types as $type){ 419 if (!@file_exists($logosrc.$type)) continue; 420 $logoID .= $type; 421 $logosrc .= $type; 422 list($w, $h, $t, $a) = getimagesize($logosrc); 423 return ' style="min-height: '.$h.'px">'. 424 '<img class="mediaright" src="'.ml($logoID).'" alt="'.$tag[2][0].'"/'; 425 } 426 return ''; 427 } 428 429 /** 430 * Display an edit button for the included page 431 */ 432 function _editButton(){ 433 if ($this->page['exists']){ 434 if (($this->page['perm'] >= AUTH_EDIT) && (is_writable($this->page['file']))) 435 $action = 'edit'; 436 else return ''; 437 } elseif ($this->page['perm'] >= AUTH_CREATE){ 438 $action = 'create'; 439 } 440 if ($this->getConf('showeditbtn')){ 441 return '<div class="secedit">'.DOKU_LF.DOKU_TAB. 442 html_btn($action, $this->page['id'], '', array('do' => 'edit'), 'post').DOKU_LF. 443 '</div>'.DOKU_LF; 444 } else { 445 return ''; 446 } 447 } 448 449 /** 450 * Returns the meta line below the included page 451 */ 452 function _footer($page){ 453 global $conf; 454 455 if (!$this->footer) return ''; // '<div class="inclmeta"> </div>'.DOKU_LF; 456 457 $id = $page['id']; 458 $meta = p_get_metadata($id); 459 $ret = array(); 460 461 // permalink 462 if ($this->getConf('showlink')){ 463 $title = ($page['title'] ? $page['title'] : $meta['title']); 464 if (!$title) $title = str_replace('_', ' ', noNS($id)); 465 $class = ($page['exists'] ? 'wikilink1' : 'wikilink2'); 466 $link = array( 467 'url' => wl($id), 468 'title' => $id, 469 'name' => hsc($title), 470 'target' => $conf['target']['wiki'], 471 'class' => $class.' permalink', 472 'more' => 'rel="bookmark"', 473 ); 474 $ret[] = $this->renderer->_formatLink($link); 475 } 476 477 // date 478 if ($this->getConf('showdate')){ 479 $date = ($page['date'] ? $page['date'] : $meta['date']['created']); 480 if ($date) 481 $ret[] = '<abbr class="published" title="'.gmdate('Y-m-d\TH:i:s\Z', $date).'">'. 482 date($conf['dformat'], $date). 483 '</abbr>'; 484 } 485 486 // author 487 if ($this->getConf('showuser')){ 488 $author = ($page['user'] ? $page['user'] : $meta['creator']); 489 if ($author){ 490 $userpage = cleanID($this->getConf('usernamespace').':'.$author); 491 resolve_pageid(getNS($ID), $id, $exists); 492 $class = ($exists ? 'wikilink1' : 'wikilink2'); 493 $link = array( 494 'url' => wl($userpage), 495 'title' => $userpage, 496 'name' => hsc($author), 497 'target' => $conf['target']['wiki'], 498 'class' => $class.' url fn', 499 'pre' => '<span class="vcard author">', 500 'suf' => '</span>', 501 ); 502 $ret[] = $this->renderer->_formatLink($link); 503 } 504 } 505 506 // comments - let Discussion Plugin do the work for us 507 if (!$page['section'] && $this->getConf('showcomments') 508 && (!plugin_isdisabled('discussion')) 509 && ($discussion =& plugin_load('helper', 'discussion'))){ 510 $disc = $discussion->td($id); 511 if ($disc) $ret[] = '<span class="comment">'.$disc.'</span>'; 512 } 513 514 // linkbacks - let Linkback Plugin do the work for us 515 if (!$page['section'] && $this->getConf('showlinkbacks') 516 && (!plugin_isdisabled('linkback')) 517 && ($linkback =& plugin_load('helper', 'linkback'))){ 518 $link = $linkback->td($id); 519 if ($link) $ret[] = '<span class="linkback">'.$link.'</span>'; 520 } 521 522 $ret = implode(' · ', $ret); 523 524 // tags 525 if (($this->getConf('showtags')) && ($page['tags'])){ 526 $ret = $this->page['tags'].$ret; 527 } 528 529 if (!$ret) $ret = ' '; 530 return '<div class="inclmeta">'.DOKU_LF.$ret.DOKU_LF.'</div>'.DOKU_LF; 531 } 532 533} 534 535//Setup VIM: ex: et ts=4 enc=utf-8 : 536