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