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