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