1<?php 2/** 3 * Common DokuWiki functions 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <andi@splitbrain.org> 7 */ 8 9 if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../').'/'); 10 require_once(DOKU_CONF.'dokuwiki.php'); 11 require_once(DOKU_INC.'inc/io.php'); 12 require_once(DOKU_INC.'inc/utf8.php'); 13 require_once(DOKU_INC.'inc/mail.php'); 14 require_once(DOKU_INC.'inc/parserutils.php'); 15 16/** 17 * These constants are used with the recents function 18 */ 19define('RECENTS_SKIP_DELETED',2); 20define('RECENTS_SKIP_MINORS',4); 21define('RECENTS_SKIP_SUBSPACES',8); 22 23/** 24 * Return info about the current document as associative 25 * array. 26 * 27 * @author Andreas Gohr <andi@splitbrain.org> 28 */ 29function pageinfo(){ 30 global $ID; 31 global $REV; 32 global $USERINFO; 33 global $conf; 34 35 if($_SERVER['REMOTE_USER']){ 36 $info['userinfo'] = $USERINFO; 37 $info['perm'] = auth_quickaclcheck($ID); 38 $info['subscribed'] = is_subscribed($ID,$_SERVER['REMOTE_USER']); 39 $info['client'] = $_SERVER['REMOTE_USER']; 40 41 // if some outside auth were used only REMOTE_USER is set 42 if(!$info['userinfo']['name']){ 43 $info['userinfo']['name'] = $_SERVER['REMOTE_USER']; 44 } 45 46 }else{ 47 $info['perm'] = auth_aclcheck($ID,'',null); 48 $info['subscribed'] = false; 49 $info['client'] = clientIP(true); 50 } 51 52 $info['namespace'] = getNS($ID); 53 $info['locked'] = checklock($ID); 54 $info['filepath'] = realpath(wikiFN($ID,$REV)); 55 $info['exists'] = @file_exists($info['filepath']); 56 if($REV && !$info['exists']){ 57 //check if current revision was meant 58 $cur = wikiFN($ID); 59 if(@file_exists($cur) && (@filemtime($cur) == $REV)){ 60 $info['filepath'] = realpath($cur); 61 $info['exists'] = true; 62 $REV = ''; 63 } 64 } 65 $info['rev'] = $REV; 66 if($info['exists']){ 67 $info['writable'] = (is_writable($info['filepath']) && 68 ($info['perm'] >= AUTH_EDIT)); 69 }else{ 70 $info['writable'] = ($info['perm'] >= AUTH_CREATE); 71 } 72 $info['editable'] = ($info['writable'] && empty($info['lock'])); 73 $info['lastmod'] = @filemtime($info['filepath']); 74 75 //who's the editor 76 if($REV){ 77 $revinfo = getRevisionInfo($ID,$REV); 78 }else{ 79 $revinfo = getRevisionInfo($ID,$info['lastmod']); 80 } 81 $info['ip'] = $revinfo['ip']; 82 $info['user'] = $revinfo['user']; 83 $info['sum'] = $revinfo['sum']; 84 $info['minor'] = $revinfo['minor']; 85 86 if($revinfo['user']){ 87 $info['editor'] = $revinfo['user']; 88 }else{ 89 $info['editor'] = $revinfo['ip']; 90 } 91 92 // draft 93 $draft = getCacheName($info['client'].$ID,'.draft'); 94 if(@file_exists($draft)){ 95 if(@filemtime($draft) < @filemtime(wikiFN($ID))){ 96 // remove stale draft 97 @unlink($draft); 98 }else{ 99 $info['draft'] = $draft; 100 } 101 } 102 103 return $info; 104} 105 106/** 107 * Build an string of URL parameters 108 * 109 * @author Andreas Gohr 110 */ 111function buildURLparams($params, $sep='&'){ 112 $url = ''; 113 $amp = false; 114 foreach($params as $key => $val){ 115 if($amp) $url .= $sep; 116 117 $url .= $key.'='; 118 $url .= rawurlencode($val); 119 $amp = true; 120 } 121 return $url; 122} 123 124/** 125 * Build an string of html tag attributes 126 * 127 * @author Andreas Gohr 128 */ 129function buildAttributes($params){ 130 $url = ''; 131 foreach($params as $key => $val){ 132 $url .= $key.'="'; 133 $url .= htmlspecialchars ($val); 134 $url .= '" '; 135 } 136 return $url; 137} 138 139 140/** 141 * print a message 142 * 143 * If HTTP headers were not sent yet the message is added 144 * to the global message array else it's printed directly 145 * using html_msgarea() 146 * 147 * 148 * Levels can be: 149 * 150 * -1 error 151 * 0 info 152 * 1 success 153 * 154 * @author Andreas Gohr <andi@splitbrain.org> 155 * @see html_msgarea 156 */ 157function msg($message,$lvl=0,$line='',$file=''){ 158 global $MSG; 159 $errors[-1] = 'error'; 160 $errors[0] = 'info'; 161 $errors[1] = 'success'; 162 163 if($line || $file) $message.=' ['.basename($file).':'.$line.']'; 164 165 if(!headers_sent()){ 166 if(!isset($MSG)) $MSG = array(); 167 $MSG[]=array('lvl' => $errors[$lvl], 'msg' => $message); 168 }else{ 169 $MSG = array(); 170 $MSG[]=array('lvl' => $errors[$lvl], 'msg' => $message); 171 if(function_exists('html_msgarea')){ 172 html_msgarea(); 173 }else{ 174 print "ERROR($lvl) $message"; 175 } 176 } 177} 178 179/** 180 * This builds the breadcrumb trail and returns it as array 181 * 182 * @author Andreas Gohr <andi@splitbrain.org> 183 */ 184function breadcrumbs(){ 185 // we prepare the breadcrumbs early for quick session closing 186 static $crumbs = null; 187 if($crumbs != null) return $crumbs; 188 189 global $ID; 190 global $ACT; 191 global $conf; 192 $crumbs = $_SESSION[$conf['title']]['bc']; 193 194 //first visit? 195 if (!is_array($crumbs)){ 196 $crumbs = array(); 197 } 198 //we only save on show and existing wiki documents 199 $file = wikiFN($ID); 200 if($ACT != 'show' || !@file_exists($file)){ 201 $_SESSION[$conf['title']]['bc'] = $crumbs; 202 return $crumbs; 203 } 204 205 // page names 206 $name = noNS($ID); 207 if ($conf['useheading']) { 208 // get page title 209 $title = p_get_first_heading($ID); 210 if ($title) { 211 $name = $title; 212 } 213 } 214 215 //remove ID from array 216 if (isset($crumbs[$ID])) { 217 unset($crumbs[$ID]); 218 } 219 220 //add to array 221 $crumbs[$ID] = $name; 222 //reduce size 223 while(count($crumbs) > $conf['breadcrumbs']){ 224 array_shift($crumbs); 225 } 226 //save to session 227 $_SESSION[$conf['title']]['bc'] = $crumbs; 228 return $crumbs; 229} 230 231/** 232 * Filter for page IDs 233 * 234 * This is run on a ID before it is outputted somewhere 235 * currently used to replace the colon with something else 236 * on Windows systems and to have proper URL encoding 237 * 238 * Urlencoding is ommitted when the second parameter is false 239 * 240 * @author Andreas Gohr <andi@splitbrain.org> 241 */ 242function idfilter($id,$ue=true){ 243 global $conf; 244 if ($conf['useslash'] && $conf['userewrite']){ 245 $id = strtr($id,':','/'); 246 }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && 247 $conf['userewrite']) { 248 $id = strtr($id,':',';'); 249 } 250 if($ue){ 251 $id = rawurlencode($id); 252 $id = str_replace('%3A',':',$id); //keep as colon 253 $id = str_replace('%2F','/',$id); //keep as slash 254 } 255 return $id; 256} 257 258/** 259 * This builds a link to a wikipage 260 * 261 * It handles URL rewriting and adds additional parameter if 262 * given in $more 263 * 264 * @author Andreas Gohr <andi@splitbrain.org> 265 */ 266function wl($id='',$more='',$abs=false,$sep='&'){ 267 global $conf; 268 if(is_array($more)){ 269 $more = buildURLparams($more,$sep); 270 }else{ 271 $more = str_replace(',',$sep,$more); 272 } 273 274 $id = idfilter($id); 275 if($abs){ 276 $xlink = DOKU_URL; 277 }else{ 278 $xlink = DOKU_BASE; 279 } 280 281 if($conf['userewrite'] == 2){ 282 $xlink .= DOKU_SCRIPT.'/'.$id; 283 if($more) $xlink .= '?'.$more; 284 }elseif($conf['userewrite']){ 285 $xlink .= $id; 286 if($more) $xlink .= '?'.$more; 287 }else{ 288 $xlink .= DOKU_SCRIPT.'?id='.$id; 289 if($more) $xlink .= $sep.$more; 290 } 291 292 return $xlink; 293} 294 295/** 296 * Build a link to a media file 297 * 298 * Will return a link to the detail page if $direct is false 299 */ 300function ml($id='',$more='',$direct=true,$sep='&'){ 301 global $conf; 302 if(is_array($more)){ 303 $more = buildURLparams($more,$sep); 304 }else{ 305 $more = str_replace(',',$sep,$more); 306 } 307 308 $xlink = DOKU_BASE; 309 310 // external URLs are always direct without rewriting 311 if(preg_match('#^(https?|ftp)://#i',$id)){ 312 $xlink .= 'lib/exe/fetch.php'; 313 if($more){ 314 $xlink .= '?'.$more; 315 $xlink .= $sep.'media='.rawurlencode($id); 316 }else{ 317 $xlink .= '?media='.rawurlencode($id); 318 } 319 return $xlink; 320 } 321 322 $id = idfilter($id); 323 324 // decide on scriptname 325 if($direct){ 326 if($conf['userewrite'] == 1){ 327 $script = '_media'; 328 }else{ 329 $script = 'lib/exe/fetch.php'; 330 } 331 }else{ 332 if($conf['userewrite'] == 1){ 333 $script = '_detail'; 334 }else{ 335 $script = 'lib/exe/detail.php'; 336 } 337 } 338 339 // build URL based on rewrite mode 340 if($conf['userewrite']){ 341 $xlink .= $script.'/'.$id; 342 if($more) $xlink .= '?'.$more; 343 }else{ 344 if($more){ 345 $xlink .= $script.'?'.$more; 346 $xlink .= $sep.'media='.$id; 347 }else{ 348 $xlink .= $script.'?media='.$id; 349 } 350 } 351 352 return $xlink; 353} 354 355 356 357/** 358 * Just builds a link to a script 359 * 360 * @todo maybe obsolete 361 * @author Andreas Gohr <andi@splitbrain.org> 362 */ 363function script($script='doku.php'){ 364# $link = getBaseURL(); 365# $link .= $script; 366# return $link; 367 return DOKU_BASE.DOKU_SCRIPT; 368} 369 370/** 371 * Spamcheck against wordlist 372 * 373 * Checks the wikitext against a list of blocked expressions 374 * returns true if the text contains any bad words 375 * 376 * @author Andreas Gohr <andi@splitbrain.org> 377 */ 378function checkwordblock(){ 379 global $TEXT; 380 global $conf; 381 382 if(!$conf['usewordblock']) return false; 383 384 $wordblocks = getWordblocks(); 385 //how many lines to read at once (to work around some PCRE limits) 386 if(version_compare(phpversion(),'4.3.0','<')){ 387 //old versions of PCRE define a maximum of parenthesises even if no 388 //backreferences are used - the maximum is 99 389 //this is very bad performancewise and may even be too high still 390 $chunksize = 40; 391 }else{ 392 //read file in chunks of 600 - this should work around the 393 //MAX_PATTERN_SIZE in modern PCRE 394 $chunksize = 400; 395 } 396 while($blocks = array_splice($wordblocks,0,$chunksize)){ 397 $re = array(); 398 #build regexp from blocks 399 foreach($blocks as $block){ 400 $block = preg_replace('/#.*$/','',$block); 401 $block = trim($block); 402 if(empty($block)) continue; 403 $re[] = $block; 404 } 405 if(preg_match('#('.join('|',$re).')#si',$TEXT, $match=array())) { 406 return true; 407 } 408 } 409 return false; 410} 411 412/** 413 * Return the IP of the client 414 * 415 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 416 * 417 * It returns a comma separated list of IPs if the above mentioned 418 * headers are set. If the single parameter is set, it tries to return 419 * a routable public address, prefering the ones suplied in the X 420 * headers 421 * 422 * @param boolean $single If set only a single IP is returned 423 * @author Andreas Gohr <andi@splitbrain.org> 424 */ 425function clientIP($single=false){ 426 $ip = array(); 427 $ip[] = $_SERVER['REMOTE_ADDR']; 428 if($_SERVER['HTTP_X_FORWARDED_FOR']) 429 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR'])); 430 if($_SERVER['HTTP_X_REAL_IP']) 431 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP'])); 432 433 // remove any non-IP stuff 434 $cnt = count($ip); 435 for($i=0; $i<$cnt; $i++){ 436 $ip[$i] = preg_replace('/[^0-9\.]+/','',$ip[$i]); 437 if(!preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i])) $ip[$i] = ''; 438 if(empty($ip[$i])) unset($ip[$i]); 439 } 440 $ip = array_values(array_unique($ip)); 441 if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 442 443 if(!$single) return join(',',$ip); 444 445 // decide which IP to use, trying to avoid local addresses 446 $ip = array_reverse($ip); 447 foreach($ip as $i){ 448 if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){ 449 continue; 450 }else{ 451 return $i; 452 } 453 } 454 // still here? just use the first (last) address 455 return $ip[0]; 456} 457 458/** 459 * Checks if a given page is currently locked. 460 * 461 * removes stale lockfiles 462 * 463 * @author Andreas Gohr <andi@splitbrain.org> 464 */ 465function checklock($id){ 466 global $conf; 467 $lock = wikiFN($id).'.lock'; 468 469 //no lockfile 470 if(!@file_exists($lock)) return false; 471 472 //lockfile expired 473 if((time() - filemtime($lock)) > $conf['locktime']){ 474 unlink($lock); 475 return false; 476 } 477 478 //my own lock 479 $ip = io_readFile($lock); 480 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 481 return false; 482 } 483 484 return $ip; 485} 486 487/** 488 * Lock a page for editing 489 * 490 * @author Andreas Gohr <andi@splitbrain.org> 491 */ 492function lock($id){ 493 $lock = wikiFN($id).'.lock'; 494 if($_SERVER['REMOTE_USER']){ 495 io_saveFile($lock,$_SERVER['REMOTE_USER']); 496 }else{ 497 io_saveFile($lock,clientIP()); 498 } 499} 500 501/** 502 * Unlock a page if it was locked by the user 503 * 504 * @author Andreas Gohr <andi@splitbrain.org> 505 * @return bool true if a lock was removed 506 */ 507function unlock($id){ 508 $lock = wikiFN($id).'.lock'; 509 if(@file_exists($lock)){ 510 $ip = io_readFile($lock); 511 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 512 @unlink($lock); 513 return true; 514 } 515 } 516 return false; 517} 518 519/** 520 * convert line ending to unix format 521 * 522 * @see formText() for 2crlf conversion 523 * @author Andreas Gohr <andi@splitbrain.org> 524 */ 525function cleanText($text){ 526 $text = preg_replace("/(\015\012)|(\015)/","\012",$text); 527 return $text; 528} 529 530/** 531 * Prepares text for print in Webforms by encoding special chars. 532 * It also converts line endings to Windows format which is 533 * pseudo standard for webforms. 534 * 535 * @see cleanText() for 2unix conversion 536 * @author Andreas Gohr <andi@splitbrain.org> 537 */ 538function formText($text){ 539 $text = preg_replace("/\012/","\015\012",$text); 540 return htmlspecialchars($text); 541} 542 543/** 544 * Returns the specified local text in raw format 545 * 546 * @author Andreas Gohr <andi@splitbrain.org> 547 */ 548function rawLocale($id){ 549 return io_readFile(localeFN($id)); 550} 551 552/** 553 * Returns the raw WikiText 554 * 555 * @author Andreas Gohr <andi@splitbrain.org> 556 */ 557function rawWiki($id,$rev=''){ 558 return io_readFile(wikiFN($id,$rev)); 559} 560 561/** 562 * Returns the pagetemplate contents for the ID's namespace 563 * 564 * @author Andreas Gohr <andi@splitbrain.org> 565 */ 566function pageTemplate($id){ 567 global $conf; 568 global $INFO; 569 $tpl = io_readFile(dirname(wikiFN($id)).'/_template.txt'); 570 $tpl = str_replace('@ID@',$id,$tpl); 571 $tpl = str_replace('@NS@',getNS($id),$tpl); 572 $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl); 573 $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl); 574 $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl); 575 $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl); 576 $tpl = str_replace('@DATE@',date($conf['dformat']),$tpl); 577 return $tpl; 578} 579 580 581/** 582 * Returns the raw Wiki Text in three slices. 583 * 584 * The range parameter needs to have the form "from-to" 585 * and gives the range of the section in bytes - no 586 * UTF-8 awareness is needed. 587 * The returned order is prefix, section and suffix. 588 * 589 * @author Andreas Gohr <andi@splitbrain.org> 590 */ 591function rawWikiSlices($range,$id,$rev=''){ 592 list($from,$to) = split('-',$range,2); 593 $text = io_readFile(wikiFN($id,$rev)); 594 if(!$from) $from = 0; 595 if(!$to) $to = strlen($text)+1; 596 597 $slices[0] = substr($text,0,$from-1); 598 $slices[1] = substr($text,$from-1,$to-$from); 599 $slices[2] = substr($text,$to); 600 601 return $slices; 602} 603 604/** 605 * Joins wiki text slices 606 * 607 * function to join the text slices with correct lineendings again. 608 * When the pretty parameter is set to true it adds additional empty 609 * lines between sections if needed (used on saving). 610 * 611 * @author Andreas Gohr <andi@splitbrain.org> 612 */ 613function con($pre,$text,$suf,$pretty=false){ 614 615 if($pretty){ 616 if($pre && substr($pre,-1) != "\n") $pre .= "\n"; 617 if($suf && substr($text,-1) != "\n") $text .= "\n"; 618 } 619 620 if($pre) $pre .= "\n"; 621 if($suf) $text .= "\n"; 622 return $pre.$text.$suf; 623} 624 625/** 626 * print debug messages 627 * 628 * little function to print the content of a var 629 * 630 * @author Andreas Gohr <andi@splitbrain.org> 631 */ 632function dbg($msg,$hidden=false){ 633 (!$hidden) ? print '<pre class="dbg">' : print "<!--\n"; 634 print_r($msg); 635 (!$hidden) ? print '</pre>' : print "\n-->"; 636} 637 638/** 639 * Add's an entry to the changelog 640 * 641 * @author Andreas Gohr <andi@splitbrain.org> 642 */ 643function addLogEntry($date,$id,$summary='',$minor=false){ 644 global $conf; 645 646 if(!@is_writable($conf['changelog'])){ 647 msg($conf['changelog'].' is not writable!',-1); 648 return; 649 } 650 651 if(!$date) $date = time(); //use current time if none supplied 652 $remote = $_SERVER['REMOTE_ADDR']; 653 $user = $_SERVER['REMOTE_USER']; 654 655 if($conf['useacl'] && $user && $minor){ 656 $summary = '*'.$summary; 657 }else{ 658 $summary = ' '.$summary; 659 } 660 661 $logline = join("\t",array($date,$remote,$id,$user,$summary))."\n"; 662 io_saveFile($conf['changelog'],$logline,true); 663} 664 665/** 666 * Checks an summary entry if it was a minor edit 667 * 668 * The summary is cleaned of the marker char 669 * 670 * @author Andreas Gohr <andi@splitbrain.org> 671 */ 672function isMinor(&$summary){ 673 if(substr($summary,0,1) == '*'){ 674 $summary = substr($summary,1); 675 return true; 676 } 677 $summary = trim($summary); 678 return false; 679} 680 681/** 682 * Internal function used by getRecents 683 * 684 * don't call directly 685 * 686 * @see getRecents() 687 * @author Andreas Gohr <andi@splitbrain.org> 688 */ 689function _handleRecent($line,$ns,$flags){ 690 static $seen = array(); //caches seen pages and skip them 691 if(empty($line)) return false; //skip empty lines 692 693 // split the line into parts 694 list($dt,$ip,$id,$usr,$sum) = explode("\t",$line); 695 696 // skip seen ones 697 if($seen[$id]) return false; 698 $recent = array(); 699 700 // check minors 701 if(isMinor($sum)){ 702 // skip minors 703 if($flags & RECENTS_SKIP_MINORS) return false; 704 $recent['minor'] = true; 705 }else{ 706 $recent['minor'] = false; 707 } 708 709 // remember in seen to skip additional sights 710 $seen[$id] = 1; 711 712 // check if it's a hidden page 713 if(isHiddenPage($id)) return false; 714 715 // filter namespace 716 if (($ns) && (strpos($id,$ns.':') !== 0)) return false; 717 718 // exclude subnamespaces 719 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($id) != $ns)) return false; 720 721 // check ACL 722 if (auth_quickaclcheck($id) < AUTH_READ) return false; 723 724 // check existance 725 if(!@file_exists(wikiFN($id))){ 726 if($flags & RECENTS_SKIP_DELETED){ 727 return false; 728 }else{ 729 $recent['del'] = true; 730 } 731 }else{ 732 $recent['del'] = false; 733 } 734 735 $recent['id'] = $id; 736 $recent['date'] = $dt; 737 $recent['ip'] = $ip; 738 $recent['user'] = $usr; 739 $recent['sum'] = $sum; 740 741 return $recent; 742} 743 744 745/** 746 * returns an array of recently changed files using the 747 * changelog 748 * 749 * The following constants can be used to control which changes are 750 * included. Add them together as needed. 751 * 752 * RECENTS_SKIP_DELETED - don't include deleted pages 753 * RECENTS_SKIP_MINORS - don't include minor changes 754 * RECENTS_SKIP_SUBSPACES - don't include subspaces 755 * 756 * @param int $first number of first entry returned (for paginating 757 * @param int $num return $num entries 758 * @param string $ns restrict to given namespace 759 * @param bool $flags see above 760 * 761 * @author Andreas Gohr <andi@splitbrain.org> 762 */ 763function getRecents($first,$num,$ns='',$flags=0){ 764 global $conf; 765 $recent = array(); 766 $count = 0; 767 768 if(!$num) 769 return $recent; 770 771 if(!@is_readable($conf['changelog'])){ 772 msg($conf['changelog'].' is not readable',-1); 773 return $recent; 774 } 775 776 $fh = fopen($conf['changelog'],'r'); 777 $buf = ''; 778 $csz = 4096; //chunksize 779 fseek($fh,0,SEEK_END); // jump to the end 780 $pos = ftell($fh); // position pointer 781 782 // now read backwards into buffer 783 while($pos > 0){ 784 $pos -= $csz; // seek to previous chunk... 785 if($pos < 0) { // ...or rest of file 786 $csz += $pos; 787 $pos = 0; 788 } 789 790 fseek($fh,$pos); 791 792 $buf = fread($fh,$csz).$buf; // prepend to buffer 793 794 $lines = explode("\n",$buf); // split buffer into lines 795 796 if($pos > 0){ 797 $buf = array_shift($lines); // first one may be still incomplete 798 } 799 800 $cnt = count($lines); 801 if(!$cnt) continue; // no lines yet 802 803 // handle lines 804 for($i = $cnt-1; $i >= 0; $i--){ 805 $rec = _handleRecent($lines[$i],$ns,$flags); 806 if($rec !== false){ 807 if(--$first >= 0) continue; // skip first entries 808 $recent[] = $rec; 809 $count++; 810 811 // break while when we have enough entries 812 if($count >= $num){ 813 $pos = 0; // will break the while loop 814 break; // will break the for loop 815 } 816 } 817 } 818 }// end of while 819 820 fclose($fh); 821 return $recent; 822} 823 824/** 825 * Compare the logline $a to the timestamp $b 826 * @author Yann Hamon <yann.hamon@mandragor.org> 827 * @return integer 0 if the logline has timestamp $b, <0 if the timestam 828 * of $a is greater than $b, >0 else. 829 */ 830function hasTimestamp($a, $b) 831{ 832 if (strpos($a, $b) === 0) 833 return 0; 834 else 835 return strcmp ($a, $b); 836} 837 838/** 839 * performs a dichotomic search on an array using 840 * a custom compare function 841 * 842 * @author Yann Hamon <yann.hamon@mandragor.org> 843 */ 844function array_dichotomic_search($ar, $value, $compareFunc) { 845 $value = trim($value); 846 if (!$ar || !$value || !$compareFunc) return (null); 847 $len = count($ar); 848 849 $l = 0; 850 $r = $len-1; 851 852 do { 853 $i = floor(($l+$r)/2); 854 if ($compareFunc($ar[$i], $value)<0) 855 $l = $i+1; 856 else 857 $r = $i-1; 858 } while ($compareFunc($ar[$i], $value)!=0 && $l<=$r); 859 860 if ($compareFunc($ar[$i], $value)==0) 861 return $i; 862 else 863 return -1; 864} 865 866/** 867 * gets additonal informations for a certain pagerevison 868 * from the changelog 869 * 870 * @author Andreas Gohr <andi@splitbrain.org> 871 * @author Yann Hamon <yann.hamon@mandragor.org> 872 */ 873function getRevisionInfo($id,$rev){ 874 global $conf; 875 876 if(!$rev) return(null); 877 878 $info = array(); 879 if(!@is_readable($conf['changelog'])){ 880 msg($conf['changelog'].' is not readable',-1); 881 return $recent; 882 } 883 $loglines = file($conf['changelog']); 884 885 // Search for a line with a matching timestamp 886 $index = array_dichotomic_search ($loglines, $rev, hasTimestamp); 887 if ($index == -1) 888 return; 889 890 // The following code is necessary when there is more than 891 // one line with one same timestamp 892 $loglines_matching = array(); 893 $loglines_matching[] = $loglines[$index]; 894 for ($i=$index-1;hasTimestamp($loglines[$i], $rev) == 0; $i--) 895 $loglines_matching[] = $loglines[$i]; 896 for ($i=$index+1;hasTimestamp($loglines[$i], $rev) == 0; $i++) 897 $loglines_matching[] = $loglines[$i]; 898 899 // Match only lines concerning the document $id 900 $loglines_matching = preg_grep("/$rev\t\d+\.\d+\.\d+\.\d+\t$id\t/",$loglines_matching); 901 902 $loglines_matching = array_reverse($loglines_matching); //reverse sort on timestamp 903 $line = split("\t",$loglines_matching[0]); 904 905 $info['date'] = $line[0]; 906 $info['ip'] = $line[1]; 907 $info['user'] = $line[3]; 908 $info['sum'] = $line[4]; 909 $info['minor'] = isMinor($info['sum']); 910 return $info; 911} 912 913 914/** 915 * Saves a wikitext by calling io_saveFile 916 * 917 * @author Andreas Gohr <andi@splitbrain.org> 918 */ 919function saveWikiText($id,$text,$summary,$minor=false){ 920 global $conf; 921 global $lang; 922 // ignore if no changes were made 923 if($text == rawWiki($id,'')){ 924 return; 925 } 926 927 $file = wikiFN($id); 928 $old = saveOldRevision($id); 929 930 if (empty($text)){ 931 // remove empty file 932 @unlink($file); 933 // remove any meta info 934 $mfiles = metaFiles($id); 935 foreach ($mfiles as $mfile) { 936 if (file_exists($mfile)) @unlink($mfile); 937 } 938 $del = true; 939 // autoset summary on deletion 940 if(empty($summary)) $summary = $lang['deleted']; 941 // unlock early 942 unlock($id); 943 // remove empty namespaces 944 io_sweepNS($id); 945 }else{ 946 // save file (datadir is created in io_saveFile) 947 io_saveFile($file,$text); 948 $del = false; 949 } 950 951 addLogEntry(@filemtime($file),$id,$summary,$minor); 952 // send notify mails 953 notify($id,'admin',$old,$summary,$minor); 954 notify($id,'subscribers',$old,$summary,$minor); 955 956 //purge cache on add by updating the purgefile 957 if($conf['purgeonadd'] && (!$old || $del)){ 958 io_saveFile($conf['cachedir'].'/purgefile',time()); 959 } 960} 961 962/** 963 * moves the current version to the attic and returns its 964 * revision date 965 * 966 * @author Andreas Gohr <andi@splitbrain.org> 967 */ 968function saveOldRevision($id){ 969 global $conf; 970 $oldf = wikiFN($id); 971 if(!@file_exists($oldf)) return ''; 972 $date = filemtime($oldf); 973 $newf = wikiFN($id,$date); 974 if(substr($newf,-3)=='.gz'){ 975 io_saveFile($newf,rawWiki($id)); 976 }else{ 977 io_makeFileDir($newf); 978 copy($oldf, $newf); 979 } 980 return $date; 981} 982 983/** 984 * Sends a notify mail on page change 985 * 986 * @param string $id The changed page 987 * @param string $who Who to notify (admin|subscribers) 988 * @param int $rev Old page revision 989 * @param string $summary What changed 990 * @param boolean $minor Is this a minor edit? 991 * 992 * @author Andreas Gohr <andi@splitbrain.org> 993 */ 994function notify($id,$who,$rev='',$summary='',$minor=false){ 995 global $lang; 996 global $conf; 997 998 // decide if there is something to do 999 if($who == 'admin'){ 1000 if(empty($conf['notify'])) return; //notify enabled? 1001 $text = rawLocale('mailtext'); 1002 $to = $conf['notify']; 1003 $bcc = ''; 1004 }elseif($who == 'subscribers'){ 1005 if(!$conf['subscribers']) return; //subscribers enabled? 1006 if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors 1007 $bcc = subscriber_addresslist($id); 1008 if(empty($bcc)) return; 1009 $to = ''; 1010 $text = rawLocale('subscribermail'); 1011 }else{ 1012 return; //just to be safe 1013 } 1014 1015 $text = str_replace('@DATE@',date($conf['dformat']),$text); 1016 $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); 1017 $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text); 1018 $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text); 1019 $text = str_replace('@NEWPAGE@',wl($id,'',true),$text); 1020 $text = str_replace('@PAGE@',$id,$text); 1021 $text = str_replace('@TITLE@',$conf['title'],$text); 1022 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 1023 $text = str_replace('@SUMMARY@',$summary,$text); 1024 $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); 1025 1026 if($rev){ 1027 $subject = $lang['mail_changed'].' '.$id; 1028 $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true),$text); 1029 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 1030 $df = new Diff(split("\n",rawWiki($id,$rev)), 1031 split("\n",rawWiki($id))); 1032 $dformat = new UnifiedDiffFormatter(); 1033 $diff = $dformat->format($df); 1034 }else{ 1035 $subject=$lang['mail_newpage'].' '.$id; 1036 $text = str_replace('@OLDPAGE@','none',$text); 1037 $diff = rawWiki($id); 1038 } 1039 $text = str_replace('@DIFF@',$diff,$text); 1040 $subject = '['.$conf['title'].'] '.$subject; 1041 1042 mail_send($to,$subject,$text,$conf['mailfrom'],'',$bcc); 1043} 1044 1045/** 1046 * Return a list of available page revisons 1047 * 1048 * @author Andreas Gohr <andi@splitbrain.org> 1049 */ 1050function getRevisions($id){ 1051 $revd = dirname(wikiFN($id,'foo')); 1052 $revs = array(); 1053 $clid = cleanID($id); 1054 if(strrpos($clid,':')) $clid = substr($clid,strrpos($clid,':')+1); //remove path 1055 $clid = utf8_encodeFN($clid); 1056 1057 if (is_dir($revd) && $dh = opendir($revd)) { 1058 while (($file = readdir($dh)) !== false) { 1059 if (is_dir($revd.'/'.$file)) continue; 1060 if (preg_match('/^'.$clid.'\.(\d+)\.txt(\.gz)?$/',$file,$match)){ 1061 $revs[]=$match[1]; 1062 } 1063 } 1064 closedir($dh); 1065 } 1066 rsort($revs); 1067 return $revs; 1068} 1069 1070/** 1071 * extracts the query from a google referer 1072 * 1073 * @todo should be more generic and support yahoo et al 1074 * @author Andreas Gohr <andi@splitbrain.org> 1075 */ 1076function getGoogleQuery(){ 1077 $url = parse_url($_SERVER['HTTP_REFERER']); 1078 if(!$url) return ''; 1079 1080 if(!preg_match("#google\.#i",$url['host'])) return ''; 1081 $query = array(); 1082 parse_str($url['query'],$query); 1083 1084 return $query['q']; 1085} 1086 1087/** 1088 * Try to set correct locale 1089 * 1090 * @deprecated No longer used 1091 * @author Andreas Gohr <andi@splitbrain.org> 1092 */ 1093function setCorrectLocale(){ 1094 global $conf; 1095 global $lang; 1096 1097 $enc = strtoupper($lang['encoding']); 1098 foreach ($lang['locales'] as $loc){ 1099 //try locale 1100 if(@setlocale(LC_ALL,$loc)) return; 1101 //try loceale with encoding 1102 if(@setlocale(LC_ALL,"$loc.$enc")) return; 1103 } 1104 //still here? try to set from environment 1105 @setlocale(LC_ALL,""); 1106} 1107 1108/** 1109 * Return the human readable size of a file 1110 * 1111 * @param int $size A file size 1112 * @param int $dec A number of decimal places 1113 * @author Martin Benjamin <b.martin@cybernet.ch> 1114 * @author Aidan Lister <aidan@php.net> 1115 * @version 1.0.0 1116 */ 1117function filesize_h($size, $dec = 1){ 1118 $sizes = array('B', 'KB', 'MB', 'GB'); 1119 $count = count($sizes); 1120 $i = 0; 1121 1122 while ($size >= 1024 && ($i < $count - 1)) { 1123 $size /= 1024; 1124 $i++; 1125 } 1126 1127 return round($size, $dec) . ' ' . $sizes[$i]; 1128} 1129 1130/** 1131 * return an obfuscated email address in line with $conf['mailguard'] setting 1132 * 1133 * @author Harry Fuecks <hfuecks@gmail.com> 1134 * @author Christopher Smith <chris@jalakai.co.uk> 1135 */ 1136function obfuscate($email) { 1137 global $conf; 1138 1139 switch ($conf['mailguard']) { 1140 case 'visible' : 1141 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1142 return strtr($email, $obfuscate); 1143 1144 case 'hex' : 1145 $encode = ''; 1146 for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';'; 1147 return $encode; 1148 1149 case 'none' : 1150 default : 1151 return $email; 1152 } 1153} 1154 1155/** 1156 * Return DokuWikis version 1157 * 1158 * @author Andreas Gohr <andi@splitbrain.org> 1159 */ 1160function getVersion(){ 1161 //import version string 1162 if(@file_exists('VERSION')){ 1163 //official release 1164 return 'Release '.trim(io_readfile(DOKU_INC.'/VERSION')); 1165 }elseif(is_dir('_darcs')){ 1166 //darcs checkout 1167 $inv = file('_darcs/inventory'); 1168 $inv = preg_grep('#\*\*\d{14}[\]$]#',$inv); 1169 $cur = array_pop($inv); 1170 preg_match('#\*\*(\d{4})(\d{2})(\d{2})#',$cur,$matches); 1171 return 'Darcs '.$matches[1].'-'.$matches[2].'-'.$matches[3]; 1172 }else{ 1173 return 'snapshot?'; 1174 } 1175} 1176 1177/** 1178 * Run a few sanity checks 1179 * 1180 * @author Andreas Gohr <andi@splitbrain.org> 1181 */ 1182function check(){ 1183 global $conf; 1184 global $INFO; 1185 1186 msg('DokuWiki version: '.getVersion(),1); 1187 1188 if(version_compare(phpversion(),'4.3.0','<')){ 1189 msg('Your PHP version is too old ('.phpversion().' vs. 4.3.+ recommended)',-1); 1190 }elseif(version_compare(phpversion(),'4.3.10','<')){ 1191 msg('Consider upgrading PHP to 4.3.10 or higher for security reasons (your version: '.phpversion().')',0); 1192 }else{ 1193 msg('PHP version '.phpversion(),1); 1194 } 1195 1196 if(is_writable($conf['changelog'])){ 1197 msg('Changelog is writable',1); 1198 }else{ 1199 msg('Changelog is not writable',-1); 1200 } 1201 1202 if(is_writable($conf['datadir'])){ 1203 msg('Datadir is writable',1); 1204 }else{ 1205 msg('Datadir is not writable',-1); 1206 } 1207 1208 if(is_writable($conf['olddir'])){ 1209 msg('Attic is writable',1); 1210 }else{ 1211 msg('Attic is not writable',-1); 1212 } 1213 1214 if(is_writable($conf['mediadir'])){ 1215 msg('Mediadir is writable',1); 1216 }else{ 1217 msg('Mediadir is not writable',-1); 1218 } 1219 1220 if(is_writable($conf['cachedir'])){ 1221 msg('Cachedir is writable',1); 1222 }else{ 1223 msg('Cachedir is not writable',-1); 1224 } 1225 1226 if(is_writable(DOKU_CONF.'users.auth.php')){ 1227 msg('conf/users.auth.php is writable',1); 1228 }else{ 1229 msg('conf/users.auth.php is not writable',0); 1230 } 1231 1232 if(function_exists('mb_strpos')){ 1233 if(defined('UTF8_NOMBSTRING')){ 1234 msg('mb_string extension is available but will not be used',0); 1235 }else{ 1236 msg('mb_string extension is available and will be used',1); 1237 } 1238 }else{ 1239 msg('mb_string extension not available - PHP only replacements will be used',0); 1240 } 1241 1242 if($conf['allowdebug']){ 1243 msg('Debugging support is enabled. If you don\'t need it you should set $conf[\'allowdebug\'] = 0',-1); 1244 }else{ 1245 msg('Debugging support is disabled',1); 1246 } 1247 1248 msg('Your current permission for this page is '.$INFO['perm'],0); 1249 1250 if(is_writable($INFO['filepath'])){ 1251 msg('The current page is writable by the webserver',0); 1252 }else{ 1253 msg('The current page is not writable by the webserver',0); 1254 } 1255 1256 if($INFO['writable']){ 1257 msg('The current page is writable by you',0); 1258 }else{ 1259 msg('The current page is not writable you',0); 1260 } 1261} 1262 1263/** 1264 * Let us know if a user is tracking a page 1265 * 1266 * @author Andreas Gohr <andi@splitbrain.org> 1267 */ 1268function is_subscribed($id,$uid){ 1269 $file=metaFN($id,'.mlist'); 1270 if (@file_exists($file)) { 1271 $mlist = file($file); 1272 $pos = array_search($uid."\n",$mlist); 1273 return is_int($pos); 1274 } 1275 1276 return false; 1277} 1278 1279/** 1280 * Return a string with the email addresses of all the 1281 * users subscribed to a page 1282 * 1283 * @author Steven Danz <steven-danz@kc.rr.com> 1284 */ 1285function subscriber_addresslist($id){ 1286 global $conf; 1287 global $auth; 1288 1289 $emails = ''; 1290 1291 if (!$conf['subscribers']) return; 1292 1293 $mlist = array(); 1294 $file=metaFN($id,'.mlist'); 1295 if (file_exists($file)) { 1296 $mlist = file($file); 1297 } 1298 if(count($mlist) > 0) { 1299 foreach ($mlist as $who) { 1300 $who = rtrim($who); 1301 $info = $auth->getUserData($who); 1302 $level = auth_aclcheck($id,$who,$info['grps']); 1303 if ($level >= AUTH_READ) { 1304 if (strcasecmp($info['mail'],$conf['notify']) != 0) { 1305 if (empty($emails)) { 1306 $emails = $info['mail']; 1307 } else { 1308 $emails = "$emails,".$info['mail']; 1309 } 1310 } 1311 } 1312 } 1313 } 1314 1315 return $emails; 1316} 1317 1318/** 1319 * Removes quoting backslashes 1320 * 1321 * @author Andreas Gohr <andi@splitbrain.org> 1322 */ 1323function unslash($string,$char="'"){ 1324 return str_replace('\\'.$char,$char,$string); 1325} 1326 1327//Setup VIM: ex: et ts=2 enc=utf-8 : 1328