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); 100 }else{ 101 $revinfo = getRevisionInfo($ID,$info['lastmod']); 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 */ 932function getRevisionInfo($id,$rev){ 933 global $conf; 934 935 if(!$rev) return(null); 936 937 $info = array(); 938 if(!@is_readable($conf['changelog'])){ 939 msg($conf['changelog'].' is not readable',-1); 940 return $recent; 941 } 942 $loglines = file($conf['changelog']); 943 944 // Search for a line with a matching timestamp 945 $index = array_dichotomic_search ($loglines, $rev, hasTimestamp); 946 if ($index == -1) 947 return; 948 949 // The following code is necessary when there is more than 950 // one line with one same timestamp 951 $loglines_matching = array(); 952 $loglines_matching[] = $loglines[$index]; 953 for ($i=$index-1;$i>=0 && hasTimestamp($loglines[$i], $rev) == 0; $i--) 954 $loglines_matching[] = $loglines[$i]; 955 $logsize = count($loglines); 956 for ($i=$index+1;$i<$logsize && hasTimestamp($loglines[$i], $rev) == 0; $i++) 957 $loglines_matching[] = $loglines[$i]; 958 959 // Match only lines concerning the document $id 960 $loglines_matching = preg_grep("/$rev\t\d+\.\d+\.\d+\.\d+\t$id\t/",$loglines_matching); 961 962 $loglines_matching = array_reverse($loglines_matching); //reverse sort on timestamp 963 $line = split("\t",$loglines_matching[0]); 964 965 $info['date'] = $line[0]; 966 $info['ip'] = $line[1]; 967 $info['user'] = $line[3]; 968 $info['sum'] = $line[4]; 969 $info['minor'] = isMinor($info['sum']); 970 return $info; 971} 972 973 974/** 975 * Saves a wikitext by calling io_saveFile 976 * 977 * @author Andreas Gohr <andi@splitbrain.org> 978 */ 979function saveWikiText($id,$text,$summary,$minor=false){ 980 global $conf; 981 global $lang; 982 // ignore if no changes were made 983 if($text == rawWiki($id,'')){ 984 return; 985 } 986 987 $file = wikiFN($id); 988 $old = saveOldRevision($id); 989 990 if (empty($text)){ 991 // remove empty file 992 @unlink($file); 993 // remove any meta info 994 $mfiles = metaFiles($id); 995 foreach ($mfiles as $mfile) { 996 if (file_exists($mfile)) @unlink($mfile); 997 } 998 $del = true; 999 // autoset summary on deletion 1000 if(empty($summary)) $summary = $lang['deleted']; 1001 // unlock early 1002 unlock($id); 1003 // remove empty namespaces 1004 io_sweepNS($id); 1005 }else{ 1006 // save file (datadir is created in io_saveFile) 1007 io_saveFile($file,$text); 1008 saveMetadata($id, $file, $minor); 1009 $del = false; 1010 } 1011 1012 addLogEntry(@filemtime($file),$id,$summary,$minor); 1013 // send notify mails 1014 notify($id,'admin',$old,$summary,$minor); 1015 notify($id,'subscribers',$old,$summary,$minor); 1016 1017 //purge cache on add by updating the purgefile 1018 if($conf['purgeonadd'] && (!$old || $del)){ 1019 io_saveFile($conf['cachedir'].'/purgefile',time()); 1020 } 1021} 1022 1023/** 1024 * saves the metadata for a page 1025 * 1026 * @author Esther Brunner <wikidesign@gmail.com> 1027 */ 1028function saveMetadata($id, $file, $minor){ 1029 global $INFO; 1030 1031 $user = $_SERVER['REMOTE_USER']; 1032 1033 $meta = array(); 1034 if (!$INFO['exists']){ // newly created 1035 $meta['date']['created'] = @filectime($file); 1036 if ($user) $meta['creator'] = $INFO['userinfo']['name']; 1037 } elseif (!$minor) { // non-minor modification 1038 $meta['date']['modified'] = @filemtime($file); 1039 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 1040 } 1041 p_set_metadata($id, $meta, true); 1042} 1043 1044/** 1045 * moves the current version to the attic and returns its 1046 * revision date 1047 * 1048 * @author Andreas Gohr <andi@splitbrain.org> 1049 */ 1050function saveOldRevision($id){ 1051 global $conf; 1052 $oldf = wikiFN($id); 1053 if(!@file_exists($oldf)) return ''; 1054 $date = filemtime($oldf); 1055 $newf = wikiFN($id,$date); 1056 if(substr($newf,-3)=='.gz'){ 1057 io_saveFile($newf,rawWiki($id)); 1058 }else{ 1059 io_makeFileDir($newf); 1060 copy($oldf, $newf); 1061 } 1062 return $date; 1063} 1064 1065/** 1066 * Sends a notify mail on page change 1067 * 1068 * @param string $id The changed page 1069 * @param string $who Who to notify (admin|subscribers) 1070 * @param int $rev Old page revision 1071 * @param string $summary What changed 1072 * @param boolean $minor Is this a minor edit? 1073 * 1074 * @author Andreas Gohr <andi@splitbrain.org> 1075 */ 1076function notify($id,$who,$rev='',$summary='',$minor=false){ 1077 global $lang; 1078 global $conf; 1079 1080 // decide if there is something to do 1081 if($who == 'admin'){ 1082 if(empty($conf['notify'])) return; //notify enabled? 1083 $text = rawLocale('mailtext'); 1084 $to = $conf['notify']; 1085 $bcc = ''; 1086 }elseif($who == 'subscribers'){ 1087 if(!$conf['subscribers']) return; //subscribers enabled? 1088 if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors 1089 $bcc = subscriber_addresslist($id); 1090 if(empty($bcc)) return; 1091 $to = ''; 1092 $text = rawLocale('subscribermail'); 1093 }else{ 1094 return; //just to be safe 1095 } 1096 1097 $text = str_replace('@DATE@',date($conf['dformat']),$text); 1098 $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); 1099 $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text); 1100 $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text); 1101 $text = str_replace('@NEWPAGE@',wl($id,'',true),$text); 1102 $text = str_replace('@PAGE@',$id,$text); 1103 $text = str_replace('@TITLE@',$conf['title'],$text); 1104 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 1105 $text = str_replace('@SUMMARY@',$summary,$text); 1106 $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); 1107 1108 if($rev){ 1109 $subject = $lang['mail_changed'].' '.$id; 1110 $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true),$text); 1111 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 1112 $df = new Diff(split("\n",rawWiki($id,$rev)), 1113 split("\n",rawWiki($id))); 1114 $dformat = new UnifiedDiffFormatter(); 1115 $diff = $dformat->format($df); 1116 }else{ 1117 $subject=$lang['mail_newpage'].' '.$id; 1118 $text = str_replace('@OLDPAGE@','none',$text); 1119 $diff = rawWiki($id); 1120 } 1121 $text = str_replace('@DIFF@',$diff,$text); 1122 $subject = '['.$conf['title'].'] '.$subject; 1123 1124 mail_send($to,$subject,$text,$conf['mailfrom'],'',$bcc); 1125} 1126 1127/** 1128 * Return a list of available page revisons 1129 * 1130 * @author Andreas Gohr <andi@splitbrain.org> 1131 */ 1132function getRevisions($id){ 1133 $revd = dirname(wikiFN($id,'foo')); 1134 $revs = array(); 1135 $clid = cleanID($id); 1136 if(strrpos($clid,':')) $clid = substr($clid,strrpos($clid,':')+1); //remove path 1137 $clid = utf8_encodeFN($clid); 1138 1139 if (is_dir($revd) && $dh = opendir($revd)) { 1140 while (($file = readdir($dh)) !== false) { 1141 if (is_dir($revd.'/'.$file)) continue; 1142 if (preg_match('/^'.$clid.'\.(\d+)\.txt(\.gz)?$/',$file,$match)){ 1143 $revs[]=$match[1]; 1144 } 1145 } 1146 closedir($dh); 1147 } 1148 rsort($revs); 1149 return $revs; 1150} 1151 1152/** 1153 * extracts the query from a google referer 1154 * 1155 * @todo should be more generic and support yahoo et al 1156 * @author Andreas Gohr <andi@splitbrain.org> 1157 */ 1158function getGoogleQuery(){ 1159 $url = parse_url($_SERVER['HTTP_REFERER']); 1160 if(!$url) return ''; 1161 1162 if(!preg_match("#google\.#i",$url['host'])) return ''; 1163 $query = array(); 1164 parse_str($url['query'],$query); 1165 1166 return $query['q']; 1167} 1168 1169/** 1170 * Try to set correct locale 1171 * 1172 * @deprecated No longer used 1173 * @author Andreas Gohr <andi@splitbrain.org> 1174 */ 1175function setCorrectLocale(){ 1176 global $conf; 1177 global $lang; 1178 1179 $enc = strtoupper($lang['encoding']); 1180 foreach ($lang['locales'] as $loc){ 1181 //try locale 1182 if(@setlocale(LC_ALL,$loc)) return; 1183 //try loceale with encoding 1184 if(@setlocale(LC_ALL,"$loc.$enc")) return; 1185 } 1186 //still here? try to set from environment 1187 @setlocale(LC_ALL,""); 1188} 1189 1190/** 1191 * Return the human readable size of a file 1192 * 1193 * @param int $size A file size 1194 * @param int $dec A number of decimal places 1195 * @author Martin Benjamin <b.martin@cybernet.ch> 1196 * @author Aidan Lister <aidan@php.net> 1197 * @version 1.0.0 1198 */ 1199function filesize_h($size, $dec = 1){ 1200 $sizes = array('B', 'KB', 'MB', 'GB'); 1201 $count = count($sizes); 1202 $i = 0; 1203 1204 while ($size >= 1024 && ($i < $count - 1)) { 1205 $size /= 1024; 1206 $i++; 1207 } 1208 1209 return round($size, $dec) . ' ' . $sizes[$i]; 1210} 1211 1212/** 1213 * return an obfuscated email address in line with $conf['mailguard'] setting 1214 * 1215 * @author Harry Fuecks <hfuecks@gmail.com> 1216 * @author Christopher Smith <chris@jalakai.co.uk> 1217 */ 1218function obfuscate($email) { 1219 global $conf; 1220 1221 switch ($conf['mailguard']) { 1222 case 'visible' : 1223 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1224 return strtr($email, $obfuscate); 1225 1226 case 'hex' : 1227 $encode = ''; 1228 for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';'; 1229 return $encode; 1230 1231 case 'none' : 1232 default : 1233 return $email; 1234 } 1235} 1236 1237/** 1238 * Return DokuWikis version 1239 * 1240 * @author Andreas Gohr <andi@splitbrain.org> 1241 */ 1242function getVersion(){ 1243 //import version string 1244 if(@file_exists('VERSION')){ 1245 //official release 1246 return 'Release '.trim(io_readfile(DOKU_INC.'/VERSION')); 1247 }elseif(is_dir('_darcs')){ 1248 //darcs checkout 1249 $inv = file('_darcs/inventory'); 1250 $inv = preg_grep('#\*\*\d{14}[\]$]#',$inv); 1251 $cur = array_pop($inv); 1252 preg_match('#\*\*(\d{4})(\d{2})(\d{2})#',$cur,$matches); 1253 return 'Darcs '.$matches[1].'-'.$matches[2].'-'.$matches[3]; 1254 }else{ 1255 return 'snapshot?'; 1256 } 1257} 1258 1259/** 1260 * Run a few sanity checks 1261 * 1262 * @author Andreas Gohr <andi@splitbrain.org> 1263 */ 1264function check(){ 1265 global $conf; 1266 global $INFO; 1267 1268 msg('DokuWiki version: '.getVersion(),1); 1269 1270 if(version_compare(phpversion(),'4.3.0','<')){ 1271 msg('Your PHP version is too old ('.phpversion().' vs. 4.3.+ recommended)',-1); 1272 }elseif(version_compare(phpversion(),'4.3.10','<')){ 1273 msg('Consider upgrading PHP to 4.3.10 or higher for security reasons (your version: '.phpversion().')',0); 1274 }else{ 1275 msg('PHP version '.phpversion(),1); 1276 } 1277 1278 if(is_writable($conf['changelog'])){ 1279 msg('Changelog is writable',1); 1280 }else{ 1281 msg('Changelog is not writable',-1); 1282 } 1283 1284 if(is_writable($conf['datadir'])){ 1285 msg('Datadir is writable',1); 1286 }else{ 1287 msg('Datadir is not writable',-1); 1288 } 1289 1290 if(is_writable($conf['olddir'])){ 1291 msg('Attic is writable',1); 1292 }else{ 1293 msg('Attic is not writable',-1); 1294 } 1295 1296 if(is_writable($conf['mediadir'])){ 1297 msg('Mediadir is writable',1); 1298 }else{ 1299 msg('Mediadir is not writable',-1); 1300 } 1301 1302 if(is_writable($conf['cachedir'])){ 1303 msg('Cachedir is writable',1); 1304 }else{ 1305 msg('Cachedir is not writable',-1); 1306 } 1307 1308 if(is_writable($conf['lockdir'])){ 1309 msg('Lockdir is writable',1); 1310 }else{ 1311 msg('Lockdir is not writable',-1); 1312 } 1313 1314 if(is_writable(DOKU_CONF.'users.auth.php')){ 1315 msg('conf/users.auth.php is writable',1); 1316 }else{ 1317 msg('conf/users.auth.php is not writable',0); 1318 } 1319 1320 if(function_exists('mb_strpos')){ 1321 if(defined('UTF8_NOMBSTRING')){ 1322 msg('mb_string extension is available but will not be used',0); 1323 }else{ 1324 msg('mb_string extension is available and will be used',1); 1325 } 1326 }else{ 1327 msg('mb_string extension not available - PHP only replacements will be used',0); 1328 } 1329 1330 if($conf['allowdebug']){ 1331 msg('Debugging support is enabled. If you don\'t need it you should set $conf[\'allowdebug\'] = 0',-1); 1332 }else{ 1333 msg('Debugging support is disabled',1); 1334 } 1335 1336 msg('Your current permission for this page is '.$INFO['perm'],0); 1337 1338 if(is_writable($INFO['filepath'])){ 1339 msg('The current page is writable by the webserver',0); 1340 }else{ 1341 msg('The current page is not writable by the webserver',0); 1342 } 1343 1344 if($INFO['writable']){ 1345 msg('The current page is writable by you',0); 1346 }else{ 1347 msg('The current page is not writable you',0); 1348 } 1349} 1350 1351/** 1352 * Let us know if a user is tracking a page 1353 * 1354 * @author Andreas Gohr <andi@splitbrain.org> 1355 */ 1356function is_subscribed($id,$uid){ 1357 $file=metaFN($id,'.mlist'); 1358 if (@file_exists($file)) { 1359 $mlist = file($file); 1360 $pos = array_search($uid."\n",$mlist); 1361 return is_int($pos); 1362 } 1363 1364 return false; 1365} 1366 1367/** 1368 * Return a string with the email addresses of all the 1369 * users subscribed to a page 1370 * 1371 * @author Steven Danz <steven-danz@kc.rr.com> 1372 */ 1373function subscriber_addresslist($id){ 1374 global $conf; 1375 global $auth; 1376 1377 $emails = ''; 1378 1379 if (!$conf['subscribers']) return; 1380 1381 $mlist = array(); 1382 $file=metaFN($id,'.mlist'); 1383 if (file_exists($file)) { 1384 $mlist = file($file); 1385 } 1386 if(count($mlist) > 0) { 1387 foreach ($mlist as $who) { 1388 $who = rtrim($who); 1389 $info = $auth->getUserData($who); 1390 $level = auth_aclcheck($id,$who,$info['grps']); 1391 if ($level >= AUTH_READ) { 1392 if (strcasecmp($info['mail'],$conf['notify']) != 0) { 1393 if (empty($emails)) { 1394 $emails = $info['mail']; 1395 } else { 1396 $emails = "$emails,".$info['mail']; 1397 } 1398 } 1399 } 1400 } 1401 } 1402 1403 return $emails; 1404} 1405 1406/** 1407 * Removes quoting backslashes 1408 * 1409 * @author Andreas Gohr <andi@splitbrain.org> 1410 */ 1411function unslash($string,$char="'"){ 1412 return str_replace('\\'.$char,$char,$string); 1413} 1414 1415//Setup VIM: ex: et ts=2 enc=utf-8 : 1416