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 16require_once(DOKU_INC.'inc/search.php'); 17 18class helper_plugin_include extends DokuWiki_Plugin { // DokuWiki_Helper_Plugin 19 20 var $defaults = array(); 21 var $sec_close = true; 22 var $taghelper = null; 23 var $includes = array(); // deprecated - compatibility code for the blog plugin 24 25 /** 26 * Constructor loads default config settings once 27 */ 28 function helper_plugin_include() { 29 $this->defaults['firstsec'] = $this->getConf('firstseconly'); 30 $this->defaults['editbtn'] = $this->getConf('showeditbtn'); 31 $this->defaults['taglogos'] = $this->getConf('showtaglogos'); 32 $this->defaults['footer'] = $this->getConf('showfooter'); 33 $this->defaults['redirect'] = $this->getConf('doredirect'); 34 $this->defaults['date'] = $this->getConf('showdate'); 35 $this->defaults['user'] = $this->getConf('showuser'); 36 $this->defaults['comments'] = $this->getConf('showcomments'); 37 $this->defaults['linkbacks'] = $this->getConf('showlinkbacks'); 38 $this->defaults['tags'] = $this->getConf('showtags'); 39 $this->defaults['link'] = $this->getConf('showlink'); 40 $this->defaults['permalink'] = $this->getConf('showpermalink'); 41 $this->defaults['indent'] = $this->getConf('doindent'); 42 } 43 44 /** 45 * Available methods for other plugins 46 */ 47 function getMethods() { 48 $result = array(); 49 $result[] = array( 50 'name' => 'get_flags', 51 'desc' => 'overrides standard values for showfooter and firstseconly settings', 52 'params' => array('flags' => 'array'), 53 ); 54 return $result; 55 } 56 57 /** 58 * Overrides standard values for showfooter and firstseconly settings 59 */ 60 function get_flags($setflags) { 61 // load defaults 62 $flags = array(); 63 $flags = $this->defaults; 64 foreach ($setflags as $flag) { 65 switch ($flag) { 66 case 'footer': 67 $flags['footer'] = 1; 68 break; 69 case 'nofooter': 70 $flags['footer'] = 0; 71 break; 72 case 'firstseconly': 73 case 'firstsectiononly': 74 $flags['firstsec'] = 1; 75 break; 76 case 'fullpage': 77 $flags['firstsec'] = 0; 78 break; 79 case 'noheader': 80 $flags['noheader'] = 1; 81 break; 82 case 'editbtn': 83 case 'editbutton': 84 $flags['editbtn'] = 1; 85 break; 86 case 'noeditbtn': 87 case 'noeditbutton': 88 $flags['editbtn'] = 0; 89 break; 90 case 'permalink': 91 $flags['permalink'] = 1; 92 break; 93 case 'nopermalink': 94 $flags['permalink'] = 0; 95 break; 96 case 'redirect': 97 $flags['redirect'] = 1; 98 break; 99 case 'noredirect': 100 $flags['redirect'] = 0; 101 break; 102 case 'link': 103 $flags['link'] = 1; 104 break; 105 case 'nolink': 106 $flags['link'] = 0; 107 break; 108 case 'user': 109 $flags['user'] = 1; 110 break; 111 case 'nouser': 112 $flags['user'] = 0; 113 break; 114 case 'comments': 115 $flags['comments'] = 1; 116 break; 117 case 'nocomments': 118 $flags['comments'] = 0; 119 break; 120 case 'linkbacks': 121 $flags['linkbacks'] = 1; 122 break; 123 case 'nolinkbacks': 124 $flags['linkbacks'] = 0; 125 break; 126 case 'tags': 127 $flags['tags'] = 1; 128 break; 129 case 'notags': 130 $flags['tags'] = 0; 131 break; 132 case 'date': 133 $flags['date'] = 1; 134 break; 135 case 'nodate': 136 $flags['date'] = 0; 137 break; 138 case 'indent': 139 $flags['indent'] = 1; 140 break; 141 case 'noindent': 142 $flags['indent'] = 0; 143 break; 144 } 145 } 146 return $flags; 147 } 148 149 /** 150 * Returns the converted instructions of a give page/section 151 * 152 * @author Michael Klier <chi@chimeric.de> 153 * @author Michael Hamann <michael@content-space.de> 154 */ 155 function _get_instructions($page, $sect, $mode, $lvl, $flags, $root_id = null) { 156 $key = ($sect) ? $page . '#' . $sect : $page; 157 $this->includes[$key] = true; // legacy code for keeping compatibility with other plugins 158 159 // keep compatibility with other plugins that don't know the $root_id parameter 160 if (is_null($root_id)) { 161 global $ID; 162 $root_id = $ID; 163 } 164 165 if (page_exists($page)) { 166 $ins = p_cached_instructions(wikiFN($page)); 167 } else { 168 $ins = array(); 169 } 170 171 $this->_convert_instructions($ins, $lvl, $page, $sect, $flags, $root_id); 172 return $ins; 173 } 174 175 /** 176 * Converts instructions of the included page 177 * 178 * The funcion iterates over the given list of instructions and generates 179 * an index of header and section indicies. It also removes document 180 * start/end instructions, converts links, and removes unwanted 181 * instructions like tags, comments, linkbacks. 182 * 183 * Later all header/section levels are convertet to match the current 184 * inclusion level. 185 * 186 * @author Michael Klier <chi@chimeric.de> 187 */ 188 function _convert_instructions(&$ins, $lvl, $page, $sect, $flags, $root_id) { 189 global $conf; 190 191 // filter instructions if needed 192 if(!empty($sect)) { 193 $this->_get_section($ins, $sect); // section required 194 } 195 196 if($flags['firstsec']) { 197 $this->_get_firstsec($ins, $page); // only first section 198 } 199 200 $ns = getNS($page); 201 $num = count($ins); 202 203 $conv_idx = array(); // conversion index 204 $lvl_max = false; // max level 205 $first_header = -1; 206 $no_header = false; 207 $sect_title = false; 208 209 for($i=0; $i<$num; $i++) { 210 switch($ins[$i][0]) { 211 case 'document_start': 212 case 'document_end': 213 case 'section_edit': 214 unset($ins[$i]); 215 break; 216 case 'header': 217 // get section title of first section 218 if($sect && !$sect_title) { 219 $sect_title = $ins[$i][1][0]; 220 } 221 // check if we need to skip the first header 222 if((!$no_header) && $flags['noheader']) { 223 $no_header = true; 224 } 225 226 $conv_idx[] = $i; 227 // get index of first header 228 if($first_header == -1) $first_header = $i; 229 // get max level of this instructions set 230 if(!$lvl_max || ($ins[$i][1][1] < $lvl_max)) { 231 $lvl_max = $ins[$i][1][1]; 232 } 233 break; 234 case 'section_open': 235 $conv_idx[] = $i; 236 break; 237 case 'internallink': 238 case 'internalmedia': 239 // make sure parameters aren't touched 240 $link_params = ''; 241 $link_id = $ins[$i][1][0]; 242 $link_parts = explode('?', $link_id, 2); 243 if (count($link_parts) === 2) { 244 $link_id = $link_parts[0]; 245 $link_params = $link_parts[1]; 246 } 247 // resolve the id without cleaning it 248 $link_id = resolve_id($ns, $link_id, false); 249 // this id is internal (i.e. absolute) now, add ':' to make resolve_id work again 250 if ($link_id{0} != ':') $link_id = ':'.$link_id; 251 // restore parameters 252 $ins[$i][1][0] = ($link_params != '') ? $link_id.'?'.$link_params : $link_id; 253 break; 254 case 'plugin': 255 // FIXME skip other plugins? 256 switch($ins[$i][1][0]) { 257 case 'tag_tag': // skip tags 258 case 'discussion_comments': // skip comments 259 case 'linkback': // skip linkbacks 260 case 'data_entry': // skip data plugin 261 case 'meta': // skip meta plugin 262 unset($ins[$i]); 263 break; 264 // adapt indentation level of nested includes 265 case 'include_include': 266 if ($flags['indent']) 267 $ins[$i][1][1][4] += $lvl; 268 break; 269 } 270 break; 271 default: 272 break; 273 } 274 } 275 276 // calculate difference between header/section level and include level 277 $diff = 0; 278 if (!isset($lvl_max)) $lvl_max = 0; // if no level found in target, set to 0 279 $diff = $lvl - $lvl_max + 1; 280 if ($no_header) $diff -= 1; // push up one level if "noheader" 281 282 // convert headers and set footer/permalink 283 $hdr_deleted = false; 284 $has_permalink = false; 285 $footer_lvl = false; 286 $contains_secedit = false; 287 $section_close_at = false; 288 foreach($conv_idx as $idx) { 289 if($ins[$idx][0] == 'header') { 290 if ($section_close_at === false) { 291 // store the index of the first heading (the begin of the first section) 292 $section_close_at = $idx; 293 } 294 295 if($no_header && !$hdr_deleted) { 296 unset ($ins[$idx]); 297 $hdr_deleted = true; 298 continue; 299 } 300 301 if($flags['indent']) { 302 $lvl_new = (($ins[$idx][1][1] + $diff) > 5) ? 5 : ($ins[$idx][1][1] + $diff); 303 $ins[$idx][1][1] = $lvl_new; 304 } 305 306 if($ins[$idx][1][1] <= $conf['maxseclevel']) 307 $contains_secedit = true; 308 309 // set permalink 310 if($flags['link'] && !$has_permalink && ($idx == $first_header)) { 311 $this->_permalink($ins[$idx], $page, $sect, $flags); 312 $has_permalink = true; 313 } 314 315 // set footer level 316 if(!$footer_lvl && ($idx == $first_header) && !$no_header) { 317 if($flags['indent']) { 318 $footer_lvl = $lvl_new; 319 } else { 320 $footer_lvl = $lvl_max; 321 } 322 } 323 } else { 324 // it's a section 325 if($flags['indent']) { 326 $lvl_new = (($ins[$idx][1][0] + $diff) > 5) ? 5 : ($ins[$idx][1][0] + $diff); 327 $ins[$idx][1][0] = $lvl_new; 328 } 329 330 // check if noheader is used and set the footer level to the first section 331 if($no_header && !$footer_lvl) { 332 if($flags['indent']) { 333 $footer_lvl = $lvl_new; 334 } else { 335 $footer_lvl = $lvl_max; 336 } 337 } 338 } 339 } 340 341 // close last open section of the included page if there is any 342 if ($contains_secedit) { 343 array_push($ins, array('plugin', array('include_close_last_secedit', array()))); 344 } 345 346 // add edit button 347 if($flags['editbtn']) { 348 $perm = auth_quickaclcheck($page); 349 $can_edit = page_exists($page) ? $perm >= AUTH_EDIT : $perm >= AUTH_CREATE; 350 if ($can_edit) 351 $this->_editbtn($ins, $page, $sect, $sect_title, $root_id); 352 } 353 354 // add footer 355 if($flags['footer']) { 356 $ins[] = $this->_footer($page, $sect, $sect_title, $flags, $footer_lvl, $root_id); 357 } 358 359 // wrap content at the beginning of the include that is not in a section in a section 360 if ($lvl > 0 && $section_close_at !== 0 && $flags['indent']) { 361 if ($section_close_at === false) { 362 $ins[] = array('section_close', array()); 363 array_unshift($ins, array('section_open', array($lvl))); 364 } else { 365 $section_close_idx = array_search($section_close_at, array_keys($ins)); 366 if ($section_close_idx > 0) { 367 $before_ins = array_slice($ins, 0, $section_close_idx); 368 $after_ins = array_slice($ins, $section_close_idx); 369 $ins = array_merge($before_ins, array(array('section_close', array())), $after_ins); 370 array_unshift($ins, array('section_open', array($lvl))); 371 } 372 } 373 } 374 375 // add instructions entry wrapper 376 array_unshift($ins, array('plugin', array('include_wrap', array('open', $page)))); 377 array_push($ins, array('plugin', array('include_wrap', array('close')))); 378 379 // close previous section if any and re-open after inclusion 380 if($lvl != 0 && $this->sec_close) { 381 array_unshift($ins, array('section_close', array())); 382 $ins[] = array('section_open', array($lvl)); 383 } 384 } 385 386 /** 387 * Appends instruction item for the include plugin footer 388 * 389 * @author Michael Klier <chi@chimeric.de> 390 */ 391 function _footer($page, $sect, $sect_title, $flags, $footer_lvl, $root_id) { 392 $footer = array(); 393 $footer[0] = 'plugin'; 394 $footer[1] = array('include_footer', array($page, $sect, $sect_title, $flags, $root_id, $footer_lvl)); 395 return $footer; 396 } 397 398 /** 399 * Appends instruction item for an edit button 400 * 401 * @author Michael Klier <chi@chimeric.de> 402 */ 403 function _editbtn(&$ins, $page, $sect, $sect_title, $root_id) { 404 $editbtn = array(); 405 $editbtn[0] = 'plugin'; 406 $editbtn[1] = array('include_editbtn', array($page, $sect, $sect_title, $root_id)); 407 $ins[] = $editbtn; 408 } 409 410 /** 411 * Convert instruction item for a permalink header 412 * 413 * @author Michael Klier <chi@chimeric.de> 414 */ 415 function _permalink(&$ins, $page, $sect, $flags) { 416 $ins[0] = 'plugin'; 417 $ins[1] = array('include_header', array($ins[1][0], $ins[1][1], $page, $sect, $flags)); 418 } 419 420 /** 421 * Get a section including its subsections 422 * 423 * @author Michael Klier <chi@chimeric.de> 424 */ 425 function _get_section(&$ins, $sect) { 426 $num = count($ins); 427 $offset = false; 428 $lvl = false; 429 $end = false; 430 431 for($i=0; $i<$num; $i++) { 432 if ($ins[$i][0] == 'header') { 433 434 // found the right header 435 if (cleanID($ins[$i][1][0]) == $sect) { 436 $offset = $i; 437 $lvl = $ins[$i][1][1]; 438 } elseif ($offset && $lvl && ($ins[$i][1][1] <= $lvl)) { 439 $end = $i - $offset; 440 break; 441 } 442 } 443 } 444 $offset = $offset ? $offset : 0; 445 $end = $end ? $end : ($num - 1); 446 if(is_array($ins)) { 447 $ins = array_slice($ins, $offset, $end); 448 } 449 } 450 451 /** 452 * Only display the first section of a page and a readmore link 453 * 454 * @author Michael Klier <chi@chimeric.de> 455 */ 456 function _get_firstsec(&$ins, $page) { 457 $num = count($ins); 458 $first_sect = false; 459 for($i=0; $i<$num; $i++) { 460 if($ins[$i][0] == 'section_close') { 461 $first_sect = $i; 462 } 463 if(($first_sect) && ($ins[$i][0] == 'section_open')) { 464 $ins = array_slice($ins, 0, $first_sect); 465 $ins[] = array('p_open', array()); 466 $ins[] = array('internallink', array($page, $this->getLang('readmore'))); 467 $ins[] = array('p_close', array()); 468 $ins[] = array('section_close', array()); 469 return; 470 } 471 } 472 } 473 474 /** 475 * Gives a list of pages for a given include statement 476 * 477 * @author Michael Hamann <michael@content-space.de> 478 */ 479 function _get_included_pages($mode, $page, $sect, $parent_id) { 480 global $conf; 481 $pages = array(); 482 switch($mode) { 483 case 'namespace': 484 $ns = str_replace(':', '/', cleanID($page)); 485 search($pagearrays, $conf['datadir'], 'search_list', '', $ns); 486 if (is_array($pagearrays)) { 487 foreach ($pagearrays as $pagearray) { 488 $pages[] = $pagearray['id']; 489 } 490 } 491 break; 492 case 'tagtopic': 493 if (!$this->taghelper) 494 $this->taghelper =& plugin_load('helper', 'tag'); 495 if(!$this->taghelper) { 496 msg('You have to install the tag plugin to use this functionality!', -1); 497 return array(); 498 } 499 $tag = $page; 500 $sect = ''; 501 $pagearrays = $this->taghelper->getTopic('', null, $tag); 502 foreach ($pagearrays as $pagearray) { 503 $pages[] = $pagearray['id']; 504 } 505 break; 506 default: 507 $page = $this->_apply_macro($page); 508 resolve_pageid(getNS($parent_id), $page, $exists); // resolve shortcuts and clean ID 509 if (auth_quickaclcheck($page) >= AUTH_READ) 510 $pages[] = $page; 511 } 512 513 sort($pages); 514 515 $result = array(); 516 foreach ($pages as $page) { 517 $perm = auth_quickaclcheck($page); 518 $exists = page_exists($page); 519 $result[] = array('id' => $page, 'exists' => $exists, 'can_edit' => ($perm >= AUTH_EDIT), 'parent_id' => $parent_id); 520 } 521 return $result; 522 } 523 524 /** 525 * This function generates the list of all included pages from a list of metadata 526 * instructions. 527 */ 528 function _get_included_pages_from_meta_instructions($instructions) { 529 $pages = array(); 530 foreach ($instructions as $instruction) { 531 extract($instruction); 532 $pages = array_merge($pages, $this->_get_included_pages($mode, $page, $sect, $parent_id)); 533 } 534 return $pages; 535 } 536 537 /** 538 * Makes user or date dependent includes possible 539 */ 540 function _apply_macro($id) { 541 global $INFO; 542 global $auth; 543 544 // if we don't have an auth object, do nothing 545 if (!$auth) return $id; 546 547 $user = $_SERVER['REMOTE_USER']; 548 $group = $INFO['userinfo']['grps'][0]; 549 550 $replace = array( 551 '@USER@' => cleanID($user), 552 '@NAME@' => cleanID($INFO['userinfo']['name']), 553 '@GROUP@' => cleanID($group), 554 '@YEAR@' => date('Y'), 555 '@MONTH@' => date('m'), 556 '@DAY@' => date('d'), 557 ); 558 return str_replace(array_keys($replace), array_values($replace), $id); 559 } 560} 561// vim:ts=4:sw=4:et: 562