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