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 * @author Michael Hamann <michael@content-space.de> 8 */ 9use dokuwiki\plugin\include\GenericResolver; 10use dokuwiki\File\PageResolver; 11 12/** 13 * Helper functions for the include plugin and other plugins that want to include pages. 14 */ 15class helper_plugin_include extends DokuWiki_Plugin { // DokuWiki_Helper_Plugin 16 17 var $defaults = array(); 18 var $sec_close = true; 19 /** @var helper_plugin_tag $taghelper */ 20 var $taghelper = null; 21 var $includes = array(); // deprecated - compatibility code for the blog plugin 22 23 /** 24 * Constructor loads default config settings once 25 */ 26 function __construct() { 27 $this->defaults['noheader'] = $this->getConf('noheader'); 28 $this->defaults['firstsec'] = $this->getConf('firstseconly'); 29 $this->defaults['editbtn'] = $this->getConf('showeditbtn'); 30 $this->defaults['taglogos'] = $this->getConf('showtaglogos'); 31 $this->defaults['footer'] = $this->getConf('showfooter'); 32 $this->defaults['redirect'] = $this->getConf('doredirect'); 33 $this->defaults['date'] = $this->getConf('showdate'); 34 $this->defaults['mdate'] = $this->getConf('showmdate'); 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['order'] = $this->getConf('order'); 48 $this->defaults['rsort'] = $this->getConf('rsort'); 49 $this->defaults['depth'] = $this->getConf('depth'); 50 $this->defaults['readmore'] = $this->getConf('readmore'); 51 } 52 53 /** 54 * Available methods for other plugins 55 */ 56 function getMethods() { 57 $result = array(); 58 $result[] = array( 59 'name' => 'get_flags', 60 'desc' => 'overrides standard values for showfooter and firstseconly settings', 61 'params' => array('flags' => 'array'), 62 ); 63 return $result; 64 } 65 66 /** 67 * Overrides standard values for showfooter and firstseconly settings 68 */ 69 function get_flags($setflags) { 70 // load defaults 71 $flags = $this->defaults; 72 foreach ($setflags as $flag) { 73 $value = ''; 74 if (strpos($flag, '=') !== false) { 75 list($flag, $value) = explode('=', $flag, 2); 76 } 77 switch ($flag) { 78 case 'footer': 79 $flags['footer'] = 1; 80 break; 81 case 'nofooter': 82 $flags['footer'] = 0; 83 break; 84 case 'firstseconly': 85 case 'firstsectiononly': 86 $flags['firstsec'] = 1; 87 break; 88 case 'fullpage': 89 $flags['firstsec'] = 0; 90 break; 91 case 'showheader': 92 case 'header': 93 $flags['noheader'] = 0; 94 break; 95 case 'noheader': 96 $flags['noheader'] = 1; 97 break; 98 case 'editbtn': 99 case 'editbutton': 100 $flags['editbtn'] = 1; 101 break; 102 case 'noeditbtn': 103 case 'noeditbutton': 104 $flags['editbtn'] = 0; 105 break; 106 case 'permalink': 107 $flags['permalink'] = 1; 108 break; 109 case 'nopermalink': 110 $flags['permalink'] = 0; 111 break; 112 case 'redirect': 113 $flags['redirect'] = 1; 114 break; 115 case 'noredirect': 116 $flags['redirect'] = 0; 117 break; 118 case 'link': 119 $flags['link'] = 1; 120 break; 121 case 'nolink': 122 $flags['link'] = 0; 123 break; 124 case 'user': 125 $flags['user'] = 1; 126 break; 127 case 'nouser': 128 $flags['user'] = 0; 129 break; 130 case 'comments': 131 $flags['comments'] = 1; 132 break; 133 case 'nocomments': 134 $flags['comments'] = 0; 135 break; 136 case 'linkbacks': 137 $flags['linkbacks'] = 1; 138 break; 139 case 'nolinkbacks': 140 $flags['linkbacks'] = 0; 141 break; 142 case 'tags': 143 $flags['tags'] = 1; 144 break; 145 case 'notags': 146 $flags['tags'] = 0; 147 break; 148 case 'date': 149 $flags['date'] = 1; 150 break; 151 case 'nodate': 152 $flags['date'] = 0; 153 break; 154 case 'mdate': 155 $flags['mdate'] = 1; 156 break; 157 case 'nomdate': 158 $flags['mdate'] = 0; 159 break; 160 case 'indent': 161 $flags['indent'] = 1; 162 break; 163 case 'noindent': 164 $flags['indent'] = 0; 165 break; 166 case 'linkonly': 167 $flags['linkonly'] = 1; 168 break; 169 case 'nolinkonly': 170 case 'include_content': 171 $flags['linkonly'] = 0; 172 break; 173 case 'inline': 174 $flags['inline'] = 1; 175 break; 176 case 'title': 177 $flags['title'] = 1; 178 break; 179 case 'notitle': 180 $flags['title'] = 0; 181 break; 182 case 'pageexists': 183 $flags['pageexists'] = 1; 184 break; 185 case 'nopageexists': 186 $flags['pageexists'] = 0; 187 break; 188 case 'existlink': 189 $flags['pageexists'] = 1; 190 $flags['linkonly'] = 1; 191 break; 192 case 'parlink': 193 $flags['parlink'] = 1; 194 break; 195 case 'noparlink': 196 $flags['parlink'] = 0; 197 break; 198 case 'order': 199 $flags['order'] = $value; 200 break; 201 case 'sort': 202 $flags['rsort'] = 0; 203 break; 204 case 'rsort': 205 $flags['rsort'] = 1; 206 break; 207 case 'depth': 208 $flags['depth'] = max(intval($value), 0); 209 break; 210 case 'beforeeach': 211 $flags['beforeeach'] = $value; 212 break; 213 case 'aftereach': 214 $flags['aftereach'] = $value; 215 break; 216 case 'readmore': 217 $flags['readmore'] = 1; 218 break; 219 case 'noreadmore': 220 $flags['readmore'] = 0; 221 break; 222 case 'exclude': 223 $flags['exclude'] = $value; 224 break; 225 } 226 } 227 // the include_content URL parameter overrides flags 228 if (isset($_REQUEST['include_content'])) 229 $flags['linkonly'] = 0; 230 return $flags; 231 } 232 233 /** 234 * Returns the converted instructions of a give page/section 235 * 236 * @author Michael Klier <chi@chimeric.de> 237 * @author Michael Hamann <michael@content-space.de> 238 */ 239 function _get_instructions($page, $sect, $mode, $lvl, $flags, $root_id = null, $included_pages = array()) { 240 $key = ($sect) ? $page . '#' . $sect : $page; 241 $this->includes[$key] = true; // legacy code for keeping compatibility with other plugins 242 243 // keep compatibility with other plugins that don't know the $root_id parameter 244 if (is_null($root_id)) { 245 global $ID; 246 $root_id = $ID; 247 } 248 249 if ($flags['linkonly']) { 250 if (page_exists($page) || $flags['pageexists'] == 0) { 251 $title = ''; 252 if ($flags['title']) 253 $title = p_get_first_heading($page); 254 if($flags['parlink']) { 255 $ins = array( 256 array('p_open', array()), 257 array('internallink', array(':'.$key, $title)), 258 array('p_close', array()), 259 ); 260 } else { 261 $ins = array(array('internallink', array(':'.$key,$title))); 262 } 263 }else { 264 $ins = array(); 265 } 266 } else { 267 if (page_exists($page)) { 268 global $ID; 269 $backupID = $ID; 270 $ID = $page; // Change the global $ID as otherwise plugins like the discussion plugin will save data for the wrong page 271 $ins = p_cached_instructions(wikiFN($page), false, $page); 272 $ID = $backupID; 273 } else { 274 $ins = array(); 275 } 276 277 $this->_convert_instructions($ins, $lvl, $page, $sect, $flags, $root_id, $included_pages); 278 } 279 return $ins; 280 } 281 282 /** 283 * Converts instructions of the included page 284 * 285 * The funcion iterates over the given list of instructions and generates 286 * an index of header and section indicies. It also removes document 287 * start/end instructions, converts links, and removes unwanted 288 * instructions like tags, comments, linkbacks. 289 * 290 * Later all header/section levels are convertet to match the current 291 * inclusion level. 292 * 293 * @author Michael Klier <chi@chimeric.de> 294 */ 295 function _convert_instructions(&$ins, $lvl, $page, $sect, $flags, $root_id, $included_pages = array()) { 296 global $conf; 297 298 // filter instructions if needed 299 if(!empty($sect)) { 300 $this->_get_section($ins, $sect); // section required 301 } 302 303 if($flags['firstsec']) { 304 $this->_get_firstsec($ins, $page, $flags); // only first section 305 } 306 307 $num = count($ins); 308 309 $conv_idx = array(); // conversion index 310 $lvl_max = false; // max level 311 $first_header = -1; 312 $no_header = false; 313 $sect_title = false; 314 $endpos = null; // end position of the raw wiki text 315 316 $this->adapt_links($ins, $page, $included_pages); 317 318 for($i=0; $i<$num; $i++) { 319 switch($ins[$i][0]) { 320 case 'document_start': 321 case 'document_end': 322 case 'section_edit': 323 unset($ins[$i]); 324 break; 325 case 'header': 326 // get section title of first section 327 if($sect && !$sect_title) { 328 $sect_title = $ins[$i][1][0]; 329 } 330 // check if we need to skip the first header 331 if((!$no_header) && $flags['noheader']) { 332 $no_header = true; 333 } 334 335 $conv_idx[] = $i; 336 // get index of first header 337 if($first_header == -1) $first_header = $i; 338 // get max level of this instructions set 339 if(!$lvl_max || ($ins[$i][1][1] < $lvl_max)) { 340 $lvl_max = $ins[$i][1][1]; 341 } 342 break; 343 case 'section_open': 344 if ($flags['inline']) 345 unset($ins[$i]); 346 else 347 $conv_idx[] = $i; 348 break; 349 case 'section_close': 350 if ($flags['inline']) 351 unset($ins[$i]); 352 break; 353 case 'nest': 354 $this->adapt_links($ins[$i][1][0], $page, $included_pages); 355 break; 356 case 'plugin': 357 // FIXME skip other plugins? 358 switch($ins[$i][1][0]) { 359 case 'tag_tag': // skip tags 360 case 'discussion_comments': // skip comments 361 case 'linkback': // skip linkbacks 362 case 'data_entry': // skip data plugin 363 case 'meta': // skip meta plugin 364 case 'indexmenu_tag': // skip indexmenu sort tag 365 case 'include_sorttag': // skip include plugin sort tag 366 unset($ins[$i]); 367 break; 368 // adapt indentation level of nested includes 369 case 'include_include': 370 if (!$flags['inline'] && $flags['indent']) 371 $ins[$i][1][1][4] += $lvl; 372 break; 373 /* 374 * if there is already a closelastsecedit instruction (was added by one of the section 375 * functions), store its position but delete it as it can't be determined yet if it is needed, 376 * i.e. if there is a header which generates a section edit (depends on the levels, level 377 * adjustments, $no_header, ...) 378 */ 379 case 'include_closelastsecedit': 380 $endpos = $ins[$i][1][1][0]; 381 unset($ins[$i]); 382 break; 383 } 384 break; 385 default: 386 break; 387 } 388 } 389 390 // calculate difference between header/section level and include level 391 $diff = 0; 392 if (!isset($lvl_max)) $lvl_max = 0; // if no level found in target, set to 0 393 $diff = $lvl - $lvl_max + 1; 394 if ($no_header) $diff -= 1; // push up one level if "noheader" 395 396 // convert headers and set footer/permalink 397 $hdr_deleted = false; 398 $has_permalink = false; 399 $footer_lvl = false; 400 $contains_secedit = false; 401 $section_close_at = false; 402 foreach($conv_idx as $idx) { 403 if($ins[$idx][0] == 'header') { 404 if ($section_close_at === false && isset($ins[$idx+1]) && $ins[$idx+1][0] == 'section_open') { 405 // store the index of the first heading that is followed by a new section 406 // the wrap plugin creates sections without section_open so the section shouldn't be closed before them 407 $section_close_at = $idx; 408 } 409 410 if($no_header && !$hdr_deleted) { 411 unset ($ins[$idx]); 412 $hdr_deleted = true; 413 continue; 414 } 415 416 if($flags['indent']) { 417 $lvl_new = (($ins[$idx][1][1] + $diff) > 5) ? 5 : ($ins[$idx][1][1] + $diff); 418 $ins[$idx][1][1] = $lvl_new; 419 } 420 421 if($ins[$idx][1][1] <= $conf['maxseclevel']) 422 $contains_secedit = true; 423 424 // set permalink 425 if($flags['link'] && !$has_permalink && ($idx == $first_header)) { 426 $this->_permalink($ins[$idx], $page, $sect, $flags); 427 $has_permalink = true; 428 } 429 430 // set footer level 431 if(!$footer_lvl && ($idx == $first_header) && !$no_header) { 432 if($flags['indent'] && isset($lvl_new)) { 433 $footer_lvl = $lvl_new; 434 } else { 435 $footer_lvl = $lvl_max; 436 } 437 } 438 } else { 439 // it's a section 440 if($flags['indent']) { 441 $lvl_new = (($ins[$idx][1][0] + $diff) > 5) ? 5 : ($ins[$idx][1][0] + $diff); 442 $ins[$idx][1][0] = $lvl_new; 443 } 444 445 // check if noheader is used and set the footer level to the first section 446 if($no_header && !$footer_lvl) { 447 if($flags['indent'] && isset($lvl_new)) { 448 $footer_lvl = $lvl_new; 449 } else { 450 $footer_lvl = $lvl_max; 451 } 452 } 453 } 454 } 455 456 // close last open section of the included page if there is any 457 if ($contains_secedit) { 458 array_push($ins, array('plugin', array('include_closelastsecedit', array($endpos)))); 459 } 460 461 $include_secid = (isset($flags['include_secid']) ? $flags['include_secid'] : NULL); 462 463 // add edit button 464 if($flags['editbtn']) { 465 $this->_editbtn($ins, $page, $sect, $sect_title, ($flags['redirect'] ? $root_id : false), $include_secid); 466 } 467 468 // add footer 469 if($flags['footer']) { 470 $ins[] = $this->_footer($page, $sect, $sect_title, $flags, $footer_lvl, $root_id); 471 } 472 473 // wrap content at the beginning of the include that is not in a section in a section 474 if ($lvl > 0 && $section_close_at !== 0 && $flags['indent'] && !$flags['inline']) { 475 if ($section_close_at === false) { 476 $ins[] = array('section_close', array()); 477 array_unshift($ins, array('section_open', array($lvl))); 478 } else { 479 $section_close_idx = array_search($section_close_at, array_keys($ins)); 480 if ($section_close_idx > 0) { 481 $before_ins = array_slice($ins, 0, $section_close_idx); 482 $after_ins = array_slice($ins, $section_close_idx); 483 $ins = array_merge($before_ins, array(array('section_close', array())), $after_ins); 484 array_unshift($ins, array('section_open', array($lvl))); 485 } 486 } 487 } 488 489 // add instructions entry wrapper 490 array_unshift($ins, array('plugin', array('include_wrap', array('open', $page, $flags['redirect'], $include_secid)))); 491 if (isset($flags['beforeeach'])) 492 array_unshift($ins, array('entity', array($flags['beforeeach']))); 493 array_push($ins, array('plugin', array('include_wrap', array('close')))); 494 if (isset($flags['aftereach'])) 495 array_push($ins, array('entity', array($flags['aftereach']))); 496 497 // close previous section if any and re-open after inclusion 498 if($lvl != 0 && $this->sec_close && !$flags['inline']) { 499 array_unshift($ins, array('section_close', array())); 500 $ins[] = array('section_open', array($lvl)); 501 } 502 } 503 504 /** 505 * Appends instruction item for the include plugin footer 506 * 507 * @author Michael Klier <chi@chimeric.de> 508 */ 509 function _footer($page, $sect, $sect_title, $flags, $footer_lvl, $root_id) { 510 $footer = array(); 511 $footer[0] = 'plugin'; 512 $footer[1] = array('include_footer', array($page, $sect, $sect_title, $flags, $root_id, $footer_lvl)); 513 return $footer; 514 } 515 516 /** 517 * Appends instruction item for an edit button 518 * 519 * @author Michael Klier <chi@chimeric.de> 520 */ 521 function _editbtn(&$ins, $page, $sect, $sect_title, $root_id, $hid = '') { 522 $title = ($sect) ? $sect_title : $page; 523 $editbtn = array(); 524 $editbtn[0] = 'plugin'; 525 $editbtn[1] = array('include_editbtn', array($title, $hid)); 526 $ins[] = $editbtn; 527 } 528 529 /** 530 * Convert instruction item for a permalink header 531 * 532 * @author Michael Klier <chi@chimeric.de> 533 */ 534 function _permalink(&$ins, $page, $sect, $flags) { 535 $ins[0] = 'plugin'; 536 $ins[1] = array('include_header', array($ins[1][0], $ins[1][1], $ins[1][2], $page, $sect, $flags)); 537 } 538 539 /** 540 * Convert internal and local links depending on the included pages 541 * 542 * @param array $ins The instructions that shall be adapted 543 * @param string $page The included page 544 * @param array $included_pages The array of pages that are included 545 */ 546 private function adapt_links(&$ins, $page, $included_pages = null) { 547 $num = count($ins); 548 549 for($i=0; $i<$num; $i++) { 550 // adjust links with image titles 551 if (strpos($ins[$i][0], 'link') !== false && isset($ins[$i][1][1]) && is_array($ins[$i][1][1]) && $ins[$i][1][1]['type'] == 'internalmedia') { 552 // resolve relative ids, but without cleaning in order to preserve the name 553 $media_id = (new GenericResolver($page))->resolveId($ins[$i][1][1]['src']); 554 // make sure that after resolving the link again it will be the same link 555 if ($media_id[0] != ':') $media_id = ':'.$media_id; 556 $ins[$i][1][1]['src'] = $media_id; 557 } 558 switch($ins[$i][0]) { 559 case 'internallink': 560 case 'internalmedia': 561 // make sure parameters aren't touched 562 $link_params = ''; 563 $link_id = $ins[$i][1][0]; 564 $link_parts = explode('?', $link_id, 2); 565 if (count($link_parts) === 2) { 566 $link_id = $link_parts[0]; 567 $link_params = $link_parts[1]; 568 } 569 // resolve the id without cleaning it 570 $link_id = (new GenericResolver($page))->resolveId($link_id); 571 // this id is internal (i.e. absolute) now, add ':' to make resolve_id work again 572 if ($link_id[0] != ':') $link_id = ':'.$link_id; 573 // restore parameters 574 $ins[$i][1][0] = ($link_params != '') ? $link_id.'?'.$link_params : $link_id; 575 576 if ($ins[$i][0] == 'internallink' && !empty($included_pages)) { 577 // change links to included pages into local links 578 // only adapt links without parameters 579 $link_id = $ins[$i][1][0]; 580 $link_parts = explode('?', $link_id, 2); 581 if (count($link_parts) === 1) { 582 $link_id = (new PageResolver($page))->resolveId($link_id); 583 584 $link_parts = explode('#', $link_id, 2); 585 $hash = ''; 586 if (count($link_parts) === 2) { 587 list($link_id, $hash) = $link_parts; 588 } 589 if (array_key_exists($link_id, $included_pages)) { 590 if ($hash) { 591 // hopefully the hash is also unique in the including page (otherwise this might be the wrong link target) 592 $ins[$i][0] = 'locallink'; 593 $ins[$i][1][0] = $hash; 594 } else { 595 // the include section ids are different from normal section ids (so they won't conflict) but this 596 // also means that the normal locallink function can't be used 597 $ins[$i][0] = 'plugin'; 598 $ins[$i][1] = array('include_locallink', array($included_pages[$link_id]['hid'], $ins[$i][1][1], $ins[$i][1][0])); 599 } 600 } 601 } 602 } 603 break; 604 case 'locallink': 605 /* Convert local links to internal links if the page hasn't been fully included */ 606 if ($included_pages == null || !array_key_exists($page, $included_pages)) { 607 $ins[$i][0] = 'internallink'; 608 $ins[$i][1][0] = ':'.$page.'#'.$ins[$i][1][0]; 609 } 610 break; 611 } 612 } 613 } 614 615 /** 616 * Get a section including its subsections 617 * 618 * @author Michael Klier <chi@chimeric.de> 619 */ 620 function _get_section(&$ins, $sect) { 621 $num = count($ins); 622 $offset = false; 623 $lvl = false; 624 $end = false; 625 $endpos = null; // end position in the input text, needed for section edit buttons 626 627 $check = array(); // used for sectionID() in order to get the same ids as the xhtml renderer 628 629 for($i=0; $i<$num; $i++) { 630 if ($ins[$i][0] == 'header') { 631 632 // found the right header 633 if (sectionID($ins[$i][1][0], $check) == $sect) { 634 $offset = $i; 635 $lvl = $ins[$i][1][1]; 636 } elseif ($offset && $lvl && ($ins[$i][1][1] <= $lvl)) { 637 $end = $i - $offset; 638 $endpos = $ins[$i][1][2]; // the position directly after the found section, needed for the section edit button 639 break; 640 } 641 } 642 } 643 $offset = $offset ? $offset : 0; 644 $end = $end ? $end : ($num - 1); 645 if(is_array($ins)) { 646 $ins = array_slice($ins, $offset, $end); 647 // store the end position in the include_closelastsecedit instruction so it can generate a matching button 648 $ins[] = array('plugin', array('include_closelastsecedit', array($endpos))); 649 } 650 } 651 652 /** 653 * Only display the first section of a page and a readmore link 654 * 655 * @author Michael Klier <chi@chimeric.de> 656 */ 657 function _get_firstsec(&$ins, $page, $flags) { 658 $num = count($ins); 659 $first_sect = false; 660 $endpos = null; // end position in the input text 661 for($i=0; $i<$num; $i++) { 662 if($ins[$i][0] == 'section_close') { 663 $first_sect = $i; 664 } 665 if ($ins[$i][0] == 'header') { 666 /* 667 * Store the position of the last header that is encountered. As section_close/open-instruction are 668 * always (unless some plugin modifies this) around a header instruction this means that the last 669 * position that is stored here is exactly the position of the section_close/open at which the content 670 * is truncated. 671 */ 672 $endpos = $ins[$i][1][2]; 673 } 674 // only truncate the content and add the read more link when there is really 675 // more than that first section 676 if(($first_sect) && ($ins[$i][0] == 'section_open')) { 677 $ins = array_slice($ins, 0, $first_sect); 678 if ($flags['readmore']) { 679 $ins[] = array('plugin', array('include_readmore', array($page))); 680 } 681 $ins[] = array('section_close', array()); 682 // store the end position in the include_closelastsecedit instruction so it can generate a matching button 683 $ins[] = array('plugin', array('include_closelastsecedit', array($endpos))); 684 return; 685 } 686 } 687 } 688 689 /** 690 * Gives a list of pages for a given include statement 691 * 692 * @author Michael Hamann <michael@content-space.de> 693 */ 694 function _get_included_pages($mode, $page, $sect, $parent_id, $flags) { 695 global $conf; 696 $pages = array(); 697 switch($mode) { 698 case 'namespace': 699 $page = cleanID($page); 700 $ns = utf8_encodeFN(str_replace(':', '/', $page)); 701 // depth is absolute depth, not relative depth, but 0 has a special meaning. 702 $depth = $flags['depth'] ? $flags['depth'] + substr_count($page, ':') + ($page ? 1 : 0) : 0; 703 search($pagearrays, $conf['datadir'], 'search_allpages', array('depth' => $depth, 'skipacl' => false), $ns); 704 if (is_array($pagearrays)) { 705 foreach ($pagearrays as $pagearray) { 706 if (!isHiddenPage($pagearray['id'])) // skip hidden pages 707 $pages[] = $pagearray['id']; 708 } 709 } 710 break; 711 case 'tagtopic': 712 if (!$this->taghelper) 713 $this->taghelper = plugin_load('helper', 'tag'); 714 if(!$this->taghelper) { 715 msg('You have to install the tag plugin to use this functionality!', -1); 716 return array(); 717 } 718 $tag = $page; 719 $sect = ''; 720 $pagearrays = $this->taghelper->getTopic('', null, $tag); 721 foreach ($pagearrays as $pagearray) { 722 $pages[] = $pagearray['id']; 723 } 724 break; 725 default: 726 $page = $this->_apply_macro($page, $parent_id); 727 // resolve shortcuts and clean ID 728 $page = (new PageResolver($parent_id))->resolveId($page); 729 if (auth_quickaclcheck($page) >= AUTH_READ) 730 $pages[] = $page; 731 } 732 733 if (isset($flags['exclude'])) 734 $pages = array_filter($pages, function ($page) use ($flags) { 735 if (@preg_match($flags['exclude'], $page)) 736 return FALSE; 737 return TRUE; 738 }); 739 740 if (count($pages) > 1) { 741 if ($flags['order'] === 'id') { 742 if ($flags['rsort']) { 743 usort($pages, array($this, '_r_strnatcasecmp')); 744 } else { 745 natcasesort($pages); 746 } 747 } else { 748 $ordered_pages = array(); 749 foreach ($pages as $page) { 750 $key = ''; 751 switch ($flags['order']) { 752 case 'title': 753 $key = p_get_first_heading($page); 754 break; 755 case 'created': 756 $key = p_get_metadata($page, 'date created', METADATA_DONT_RENDER); 757 break; 758 case 'modified': 759 $key = p_get_metadata($page, 'date modified', METADATA_DONT_RENDER); 760 break; 761 case 'indexmenu': 762 $key = p_get_metadata($page, 'indexmenu_n', METADATA_RENDER_USING_SIMPLE_CACHE); 763 if ($key === null) 764 $key = ''; 765 break; 766 case 'custom': 767 $key = p_get_metadata($page, 'include_n', METADATA_RENDER_USING_SIMPLE_CACHE); 768 if ($key === null) 769 $key = ''; 770 break; 771 } 772 $key .= '_'.$page; 773 $ordered_pages[$key] = $page; 774 } 775 if ($flags['rsort']) { 776 uksort($ordered_pages, array($this, '_r_strnatcasecmp')); 777 } else { 778 uksort($ordered_pages, 'strnatcasecmp'); 779 } 780 $pages = $ordered_pages; 781 } 782 } 783 784 $result = array(); 785 foreach ($pages as $page) { 786 $exists = page_exists($page); 787 $result[] = array('id' => $page, 'exists' => $exists, 'parent_id' => $parent_id); 788 } 789 return $result; 790 } 791 792 /** 793 * String comparisons using a "natural order" algorithm in reverse order 794 * 795 * @link http://php.net/manual/en/function.strnatcmp.php 796 * @param string $a First string 797 * @param string $b Second string 798 * @return int Similar to other string comparison functions, this one returns < 0 if 799 * str1 is greater than str2; > 800 * 0 if str1 is lesser than 801 * str2, and 0 if they are equal. 802 */ 803 function _r_strnatcasecmp($a, $b) { 804 return strnatcasecmp($b, $a); 805 } 806 807 /** 808 * This function generates the list of all included pages from a list of metadata 809 * instructions. 810 */ 811 function _get_included_pages_from_meta_instructions($instructions) { 812 $pages = array(); 813 foreach ($instructions as $instruction) { 814 $mode = $instruction['mode']; 815 $page = $instruction['page']; 816 $sect = $instruction['sect']; 817 $parent_id = $instruction['parent_id']; 818 $flags = $instruction['flags']; 819 $pages = array_merge($pages, $this->_get_included_pages($mode, $page, $sect, $parent_id, $flags)); 820 } 821 return $pages; 822 } 823 824 /** 825 * Get wiki language from "HTTP_ACCEPT_LANGUAGE" 826 * We allow the pattern e.g. "ja,en-US;q=0.7,en;q=0.3" 827 */ 828 function _get_language_of_wiki($id, $parent_id) { 829 global $conf; 830 $result = $conf['lang']; 831 if(strpos($id, '@BROWSER_LANG@') !== false){ 832 $brlangp = "/([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})*|\*)(;q=(0(.[0-9]{0,3})?|1(.0{0,3})?))?/"; 833 if(preg_match_all( 834 $brlangp, $_SERVER["HTTP_ACCEPT_LANGUAGE"], 835 $matches, PREG_SET_ORDER 836 )){ 837 $langs = array(); 838 foreach($matches as $match){ 839 $langname = $match[1] == '*' ? $conf['lang'] : $match[1]; 840 $qvalue = $match[4] == '' ? 1.0 : $match[4]; 841 $langs[$langname] = $qvalue; 842 } 843 arsort($langs); 844 foreach($langs as $lang => $langq){ 845 $testpage = $this->_apply_macro(str_replace('@BROWSER_LANG@', $lang, $id), $parent_id); 846 $testpage = (new PageResolver($parent_id))->resolveId($testpage); 847 if (page_exists($testpage)) { 848 $result = $lang; 849 break; 850 } 851 } 852 } 853 } 854 return cleanID($result); 855 } 856 857 /** 858 * Makes user or date dependent includes possible 859 */ 860 function _apply_macro($id, $parent_id) { 861 global $USERINFO; 862 /* @var Input $INPUT */ 863 global $INPUT; 864 865 // The following is basicaly copied from basicinfo() because 866 // this function can be called from within pageinfo() in 867 // p_get_metadata and thus we cannot rely on $INFO being set 868 if($INPUT->server->has('REMOTE_USER')) { 869 $user = $INPUT->server->str('REMOTE_USER'); 870 } else { 871 // no registered user - use IP 872 $user = clientIP(true); 873 } 874 875 // Take user's name if possible, login name otherwise 876 if (!empty($USERINFO['name'])) { 877 $name = $USERINFO['name']; 878 } else { 879 $name = $user; 880 } 881 882 // Take first group if possible 883 if (!empty($USERINFO['grps'])) { 884 $group = $USERINFO['grps'][0]; 885 } else { 886 $group = 'ALL'; 887 } 888 889 $time_stamp = time(); 890 if(preg_match('/@DATE(\w+)@/',$id,$matches)) { 891 switch($matches[1]) { 892 case 'PMONTH': 893 $time_stamp = strtotime("-1 month"); 894 break; 895 case 'NMONTH': 896 $time_stamp = strtotime("+1 month"); 897 break; 898 case 'NWEEK': 899 $time_stamp = strtotime("+1 week"); 900 break; 901 case 'PWEEK': 902 $time_stamp = strtotime("-1 week"); 903 break; 904 case 'TOMORROW': 905 $time_stamp = strtotime("+1 day"); 906 break; 907 case 'YESTERDAY': 908 $time_stamp = strtotime("-1 day"); 909 break; 910 case 'NYEAR': 911 $time_stamp = strtotime("+1 year"); 912 break; 913 case 'PYEAR': 914 $time_stamp = strtotime("-1 year"); 915 break; 916 } 917 $id = preg_replace('/@DATE(\w+)@/','', $id); 918 } 919 920 $replace = array( 921 '@USER@' => cleanID($user), 922 '@NAME@' => cleanID($name), 923 '@GROUP@' => cleanID($group), 924 '@BROWSER_LANG@' => $this->_get_language_of_wiki($id, $parent_id), 925 '@YEAR@' => date('Y',$time_stamp), 926 '@MONTH@' => date('m',$time_stamp), 927 '@WEEK@' => date('W',$time_stamp), 928 '@DAY@' => date('d',$time_stamp), 929 '@YEARPMONTH@' => date('Ym',strtotime("-1 month")), 930 '@PMONTH@' => date('m',strtotime("-1 month")), 931 '@NMONTH@' => date('m',strtotime("+1 month")), 932 '@YEARNMONTH@' => date('Ym',strtotime("+1 month")), 933 '@YEARPWEEK@' => date('YW',strtotime("-1 week")), 934 '@PWEEK@' => date('W',strtotime("-1 week")), 935 '@NWEEK@' => date('W',strtotime("+1 week")), 936 '@YEARNWEEK@' => date('YW',strtotime("+1 week")), 937 ); 938 return str_replace(array_keys($replace), array_values($replace), $id); 939 } 940} 941// vim:ts=4:sw=4:et: 942