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 = wikiLockFN($id); 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 = wikiLockFN($id); 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 = wikiLockFN($id); 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_readWikiPage(wikiFN($id, $rev), $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_readWikiPage(wikiFN($id, $rev), $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 * Print info to a log file 699 * 700 * @author Andreas Gohr <andi@splitbrain.org> 701 */ 702function dbglog($msg){ 703 global $conf; 704 $file = $conf['cachedir'].'/debug.log'; 705 $fh = fopen($file,'a'); 706 if($fh){ 707 fwrite($fh,date('H:i:s ').$_SERVER['REMOTE_ADDR'].': '.$msg."\n"); 708 fclose($fh); 709 } 710} 711 712/** 713 * Add's an entry to the changelog 714 * 715 * @author Andreas Gohr <andi@splitbrain.org> 716 */ 717function addLogEntry($date,$id,$summary='',$minor=false){ 718 global $conf; 719 720 if(!@is_writable($conf['changelog'])){ 721 msg($conf['changelog'].' is not writable!',-1); 722 return; 723 } 724 725 if(!$date) $date = time(); //use current time if none supplied 726 $remote = $_SERVER['REMOTE_ADDR']; 727 $user = $_SERVER['REMOTE_USER']; 728 729 if($conf['useacl'] && $user && $minor){ 730 $summary = '*'.$summary; 731 }else{ 732 $summary = ' '.$summary; 733 } 734 735 $logline = join("\t",array($date,$remote,$id,$user,$summary))."\n"; 736 io_saveFile($conf['changelog'],$logline,true); 737} 738 739/** 740 * Checks an summary entry if it was a minor edit 741 * 742 * The summary is cleaned of the marker char 743 * 744 * @author Andreas Gohr <andi@splitbrain.org> 745 */ 746function isMinor(&$summary){ 747 if(substr($summary,0,1) == '*'){ 748 $summary = substr($summary,1); 749 return true; 750 } 751 $summary = trim($summary); 752 return false; 753} 754 755/** 756 * Internal function used by getRecents 757 * 758 * don't call directly 759 * 760 * @see getRecents() 761 * @author Andreas Gohr <andi@splitbrain.org> 762 */ 763function _handleRecent($line,$ns,$flags){ 764 static $seen = array(); //caches seen pages and skip them 765 if(empty($line)) return false; //skip empty lines 766 767 // split the line into parts 768 list($dt,$ip,$id,$usr,$sum) = explode("\t",$line); 769 770 // skip seen ones 771 if($seen[$id]) return false; 772 $recent = array(); 773 774 // check minors 775 if(isMinor($sum)){ 776 // skip minors 777 if($flags & RECENTS_SKIP_MINORS) return false; 778 $recent['minor'] = true; 779 }else{ 780 $recent['minor'] = false; 781 } 782 783 // remember in seen to skip additional sights 784 $seen[$id] = 1; 785 786 // check if it's a hidden page 787 if(isHiddenPage($id)) return false; 788 789 // filter namespace 790 if (($ns) && (strpos($id,$ns.':') !== 0)) return false; 791 792 // exclude subnamespaces 793 if (($flags & RECENTS_SKIP_SUBSPACES) && (getNS($id) != $ns)) return false; 794 795 // check ACL 796 if (auth_quickaclcheck($id) < AUTH_READ) return false; 797 798 // check existance 799 if(!@file_exists(wikiFN($id))){ 800 if($flags & RECENTS_SKIP_DELETED){ 801 return false; 802 }else{ 803 $recent['del'] = true; 804 } 805 }else{ 806 $recent['del'] = false; 807 } 808 809 $recent['id'] = $id; 810 $recent['date'] = $dt; 811 $recent['ip'] = $ip; 812 $recent['user'] = $usr; 813 $recent['sum'] = $sum; 814 815 return $recent; 816} 817 818 819/** 820 * returns an array of recently changed files using the 821 * changelog 822 * 823 * The following constants can be used to control which changes are 824 * included. Add them together as needed. 825 * 826 * RECENTS_SKIP_DELETED - don't include deleted pages 827 * RECENTS_SKIP_MINORS - don't include minor changes 828 * RECENTS_SKIP_SUBSPACES - don't include subspaces 829 * 830 * @param int $first number of first entry returned (for paginating 831 * @param int $num return $num entries 832 * @param string $ns restrict to given namespace 833 * @param bool $flags see above 834 * 835 * @author Andreas Gohr <andi@splitbrain.org> 836 */ 837function getRecents($first,$num,$ns='',$flags=0){ 838 global $conf; 839 $recent = array(); 840 $count = 0; 841 842 if(!$num) 843 return $recent; 844 845 if(!@is_readable($conf['changelog'])){ 846 msg($conf['changelog'].' is not readable',-1); 847 return $recent; 848 } 849 850 $fh = fopen($conf['changelog'],'r'); 851 $buf = ''; 852 $csz = 4096; //chunksize 853 fseek($fh,0,SEEK_END); // jump to the end 854 $pos = ftell($fh); // position pointer 855 856 // now read backwards into buffer 857 while($pos > 0){ 858 $pos -= $csz; // seek to previous chunk... 859 if($pos < 0) { // ...or rest of file 860 $csz += $pos; 861 $pos = 0; 862 } 863 864 fseek($fh,$pos); 865 866 $buf = fread($fh,$csz).$buf; // prepend to buffer 867 868 $lines = explode("\n",$buf); // split buffer into lines 869 870 if($pos > 0){ 871 $buf = array_shift($lines); // first one may be still incomplete 872 } 873 874 $cnt = count($lines); 875 if(!$cnt) continue; // no lines yet 876 877 // handle lines 878 for($i = $cnt-1; $i >= 0; $i--){ 879 $rec = _handleRecent($lines[$i],$ns,$flags); 880 if($rec !== false){ 881 if(--$first >= 0) continue; // skip first entries 882 $recent[] = $rec; 883 $count++; 884 885 // break while when we have enough entries 886 if($count >= $num){ 887 $pos = 0; // will break the while loop 888 break; // will break the for loop 889 } 890 } 891 } 892 }// end of while 893 894 fclose($fh); 895 return $recent; 896} 897 898/** 899 * Compare the logline $a to the timestamp $b 900 * @author Yann Hamon <yann.hamon@mandragor.org> 901 * @return integer 0 if the logline has timestamp $b, <0 if the timestam 902 * of $a is greater than $b, >0 else. 903 */ 904function hasTimestamp($a, $b) 905{ 906 if (strpos($a, $b) === 0) 907 return 0; 908 else 909 return strcmp ($a, $b); 910} 911 912/** 913 * performs a dichotomic search on an array using 914 * a custom compare function 915 * 916 * @author Yann Hamon <yann.hamon@mandragor.org> 917 */ 918function array_dichotomic_search($ar, $value, $compareFunc) { 919 $value = trim($value); 920 if (!$ar || !$value || !$compareFunc) return (null); 921 $len = count($ar); 922 923 $l = 0; 924 $r = $len-1; 925 926 do { 927 $i = floor(($l+$r)/2); 928 if ($compareFunc($ar[$i], $value)<0) 929 $l = $i+1; 930 else 931 $r = $i-1; 932 } while ($compareFunc($ar[$i], $value)!=0 && $l<=$r); 933 934 if ($compareFunc($ar[$i], $value)==0) 935 return $i; 936 else 937 return -1; 938} 939 940/** 941 * gets additonal informations for a certain pagerevison 942 * from the changelog 943 * 944 * @author Andreas Gohr <andi@splitbrain.org> 945 * @author Yann Hamon <yann.hamon@mandragor.org> 946 * @author Ben Coburn <btcoburn@silicodon.net> 947 */ 948function getRevisionInfo($id,$rev,$mem_cache=true){ 949 global $conf; 950 global $doku_temporary_revinfo_cache; 951 $cache =& $doku_temporary_revinfo_cache; 952 if(!$rev) return(null); 953 954 // check if it's already in the memory cache 955 if (is_array($cache) && isset($cache[$id]) && isset($cache[$id][$rev])) { 956 return $cache[$id][$rev]; 957 } 958 959 $info = array(); 960 if(!@is_readable($conf['changelog'])){ 961 msg($conf['changelog'].' is not readable',-1); 962 return $recent; 963 } 964 $loglines = file($conf['changelog']); 965 966 if (!$mem_cache) { 967 // Search for a line with a matching timestamp 968 $index = array_dichotomic_search($loglines, $rev, 'hasTimestamp'); 969 if ($index == -1) 970 return; 971 972 // The following code is necessary when there is more than 973 // one line with one same timestamp 974 $loglines_matching = array(); 975 for ($i=$index-1;$i>=0 && hasTimestamp($loglines[$i], $rev) == 0; $i--) 976 $loglines_matching[] = $loglines[$i]; 977 $loglines_matching = array_reverse($loglines_matching); 978 $loglines_matching[] = $loglines[$index]; 979 $logsize = count($loglines); 980 for ($i=$index+1;$i<$logsize && hasTimestamp($loglines[$i], $rev) == 0; $i++) 981 $loglines_matching[] = $loglines[$i]; 982 983 // pull off the line most recent line with the right id 984 $loglines_matching = array_reverse($loglines_matching); //newest first 985 foreach ($loglines_matching as $logline) { 986 $line = explode("\t", $logline); 987 if ($line[2]==$id) { 988 $info['date'] = $line[0]; 989 $info['ip'] = $line[1]; 990 $info['user'] = $line[3]; 991 $info['sum'] = $line[4]; 992 $info['minor'] = isMinor($info['sum']); 993 break; 994 } 995 } 996 } else { 997 // load and cache all the lines with the right id 998 if(!is_array($cache)) { $cache = array(); } 999 if (!isset($cache[$id])) { $cache[$id] = array(); } 1000 foreach ($loglines as $logline) { 1001 $start = strpos($logline, "\t", strpos($logline, "\t")+1)+1; 1002 $end = strpos($logline, "\t", $start); 1003 if (substr($logline, $start, $end-$start)==$id) { 1004 $line = explode("\t", $logline); 1005 $info = array(); 1006 $info['date'] = $line[0]; 1007 $info['ip'] = $line[1]; 1008 $info['user'] = $line[3]; 1009 $info['sum'] = $line[4]; 1010 $info['minor'] = isMinor($info['sum']); 1011 $cache[$id][$info['date']] = $info; 1012 } 1013 } 1014 $info = $cache[$id][$rev]; 1015 } 1016 1017 return $info; 1018} 1019 1020 1021/** 1022 * Saves a wikitext by calling io_writeWikiPage 1023 * 1024 * @author Andreas Gohr <andi@splitbrain.org> 1025 */ 1026function saveWikiText($id,$text,$summary,$minor=false){ 1027 global $conf; 1028 global $lang; 1029 // ignore if no changes were made 1030 if($text == rawWiki($id,'')){ 1031 return; 1032 } 1033 1034 $file = wikiFN($id); 1035 $old = saveOldRevision($id); 1036 1037 if (empty($text)){ 1038 // remove empty file 1039 @unlink($file); 1040 // remove any meta info 1041 $mfiles = metaFiles($id); 1042 foreach ($mfiles as $mfile) { 1043 if (file_exists($mfile)) @unlink($mfile); 1044 } 1045 $del = true; 1046 // autoset summary on deletion 1047 if(empty($summary)) $summary = $lang['deleted']; 1048 // remove empty namespaces 1049 io_sweepNS($id, 'datadir'); 1050 io_sweepNS($id, 'mediadir'); 1051 }else{ 1052 // save file (namespace dir is created in io_writeWikiPage) 1053 io_writeWikiPage($file, $text, $id); 1054 saveMetadata($id, $file, $minor); 1055 $del = false; 1056 } 1057 1058 addLogEntry(@filemtime($file),$id,$summary,$minor); 1059 // send notify mails 1060 notify($id,'admin',$old,$summary,$minor); 1061 notify($id,'subscribers',$old,$summary,$minor); 1062 1063 //purge cache on add by updating the purgefile 1064 if($conf['purgeonadd'] && (!$old || $del)){ 1065 io_saveFile($conf['cachedir'].'/purgefile',time()); 1066 } 1067} 1068 1069/** 1070 * saves the metadata for a page 1071 * 1072 * @author Esther Brunner <wikidesign@gmail.com> 1073 */ 1074function saveMetadata($id, $file, $minor){ 1075 global $INFO; 1076 1077 $user = $_SERVER['REMOTE_USER']; 1078 1079 $meta = array(); 1080 if (!$INFO['exists']){ // newly created 1081 $meta['date']['created'] = @filectime($file); 1082 if ($user) $meta['creator'] = $INFO['userinfo']['name']; 1083 } elseif (!$minor) { // non-minor modification 1084 $meta['date']['modified'] = @filemtime($file); 1085 if ($user) $meta['contributor'][$user] = $INFO['userinfo']['name']; 1086 } 1087 p_set_metadata($id, $meta, true); 1088} 1089 1090/** 1091 * moves the current version to the attic and returns its 1092 * revision date 1093 * 1094 * @author Andreas Gohr <andi@splitbrain.org> 1095 */ 1096function saveOldRevision($id){ 1097 global $conf; 1098 $oldf = wikiFN($id); 1099 if(!@file_exists($oldf)) return ''; 1100 $date = filemtime($oldf); 1101 $newf = wikiFN($id,$date); 1102 io_writeWikiPage($newf, rawWiki($id), $id, $date); 1103 return $date; 1104} 1105 1106/** 1107 * Sends a notify mail on page change 1108 * 1109 * @param string $id The changed page 1110 * @param string $who Who to notify (admin|subscribers) 1111 * @param int $rev Old page revision 1112 * @param string $summary What changed 1113 * @param boolean $minor Is this a minor edit? 1114 * 1115 * @author Andreas Gohr <andi@splitbrain.org> 1116 */ 1117function notify($id,$who,$rev='',$summary='',$minor=false){ 1118 global $lang; 1119 global $conf; 1120 1121 // decide if there is something to do 1122 if($who == 'admin'){ 1123 if(empty($conf['notify'])) return; //notify enabled? 1124 $text = rawLocale('mailtext'); 1125 $to = $conf['notify']; 1126 $bcc = ''; 1127 }elseif($who == 'subscribers'){ 1128 if(!$conf['subscribers']) return; //subscribers enabled? 1129 if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors 1130 $bcc = subscriber_addresslist($id); 1131 if(empty($bcc)) return; 1132 $to = ''; 1133 $text = rawLocale('subscribermail'); 1134 }elseif($who == 'register'){ 1135 if(empty($conf['registernotify'])) return; 1136 $text = rawLocale('registermail'); 1137 $to = $conf['registernotify']; 1138 $bcc = ''; 1139 }else{ 1140 return; //just to be safe 1141 } 1142 1143 $text = str_replace('@DATE@',date($conf['dformat']),$text); 1144 $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); 1145 $text = str_replace('@IPADDRESS@',$_SERVER['REMOTE_ADDR'],$text); 1146 $text = str_replace('@HOSTNAME@',gethostbyaddr($_SERVER['REMOTE_ADDR']),$text); 1147 $text = str_replace('@NEWPAGE@',wl($id,'',true),$text); 1148 $text = str_replace('@PAGE@',$id,$text); 1149 $text = str_replace('@TITLE@',$conf['title'],$text); 1150 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 1151 $text = str_replace('@SUMMARY@',$summary,$text); 1152 $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); 1153 1154 if($who == 'register'){ 1155 $subject = $lang['mail_new_user'].' '.$summary; 1156 }elseif($rev){ 1157 $subject = $lang['mail_changed'].' '.$id; 1158 $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true),$text); 1159 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 1160 $df = new Diff(split("\n",rawWiki($id,$rev)), 1161 split("\n",rawWiki($id))); 1162 $dformat = new UnifiedDiffFormatter(); 1163 $diff = $dformat->format($df); 1164 }else{ 1165 $subject=$lang['mail_newpage'].' '.$id; 1166 $text = str_replace('@OLDPAGE@','none',$text); 1167 $diff = rawWiki($id); 1168 } 1169 $text = str_replace('@DIFF@',$diff,$text); 1170 $subject = '['.$conf['title'].'] '.$subject; 1171 1172 mail_send($to,$subject,$text,$conf['mailfrom'],'',$bcc); 1173} 1174 1175/** 1176 * Return a list of available page revisons 1177 * 1178 * @author Andreas Gohr <andi@splitbrain.org> 1179 */ 1180function getRevisions($id){ 1181 $revd = dirname(wikiFN($id,'foo')); 1182 $revs = array(); 1183 $clid = cleanID($id); 1184 if(strrpos($clid,':')) $clid = substr($clid,strrpos($clid,':')+1); //remove path 1185 $clid = utf8_encodeFN($clid); 1186 $clid_len = strlen($clid); 1187 if (is_dir($revd) && $dh = opendir($revd)) { 1188 while (($file = readdir($dh)) !== false) { 1189 if (substr($file, 0, $clid_len)===$clid) { 1190 $p = @strpos($file, '.', $clid_len+1); 1191 if (!$p===false) { 1192 $revs[] = substr($file, $clid_len+1, $p-$clid_len-1); 1193 } 1194 } 1195 } 1196 closedir($dh); 1197 } 1198 rsort($revs); 1199 return $revs; 1200} 1201 1202/** 1203 * extracts the query from a google referer 1204 * 1205 * @todo should be more generic and support yahoo et al 1206 * @author Andreas Gohr <andi@splitbrain.org> 1207 */ 1208function getGoogleQuery(){ 1209 $url = parse_url($_SERVER['HTTP_REFERER']); 1210 if(!$url) return ''; 1211 1212 if(!preg_match("#google\.#i",$url['host'])) return ''; 1213 $query = array(); 1214 parse_str($url['query'],$query); 1215 1216 return $query['q']; 1217} 1218 1219/** 1220 * Try to set correct locale 1221 * 1222 * @deprecated No longer used 1223 * @author Andreas Gohr <andi@splitbrain.org> 1224 */ 1225function setCorrectLocale(){ 1226 global $conf; 1227 global $lang; 1228 1229 $enc = strtoupper($lang['encoding']); 1230 foreach ($lang['locales'] as $loc){ 1231 //try locale 1232 if(@setlocale(LC_ALL,$loc)) return; 1233 //try loceale with encoding 1234 if(@setlocale(LC_ALL,"$loc.$enc")) return; 1235 } 1236 //still here? try to set from environment 1237 @setlocale(LC_ALL,""); 1238} 1239 1240/** 1241 * Return the human readable size of a file 1242 * 1243 * @param int $size A file size 1244 * @param int $dec A number of decimal places 1245 * @author Martin Benjamin <b.martin@cybernet.ch> 1246 * @author Aidan Lister <aidan@php.net> 1247 * @version 1.0.0 1248 */ 1249function filesize_h($size, $dec = 1){ 1250 $sizes = array('B', 'KB', 'MB', 'GB'); 1251 $count = count($sizes); 1252 $i = 0; 1253 1254 while ($size >= 1024 && ($i < $count - 1)) { 1255 $size /= 1024; 1256 $i++; 1257 } 1258 1259 return round($size, $dec) . ' ' . $sizes[$i]; 1260} 1261 1262/** 1263 * return an obfuscated email address in line with $conf['mailguard'] setting 1264 * 1265 * @author Harry Fuecks <hfuecks@gmail.com> 1266 * @author Christopher Smith <chris@jalakai.co.uk> 1267 */ 1268function obfuscate($email) { 1269 global $conf; 1270 1271 switch ($conf['mailguard']) { 1272 case 'visible' : 1273 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1274 return strtr($email, $obfuscate); 1275 1276 case 'hex' : 1277 $encode = ''; 1278 for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';'; 1279 return $encode; 1280 1281 case 'none' : 1282 default : 1283 return $email; 1284 } 1285} 1286 1287/** 1288 * Return DokuWikis version 1289 * 1290 * @author Andreas Gohr <andi@splitbrain.org> 1291 */ 1292function getVersion(){ 1293 //import version string 1294 if(@file_exists('VERSION')){ 1295 //official release 1296 return 'Release '.trim(io_readfile(DOKU_INC.'/VERSION')); 1297 }elseif(is_dir('_darcs')){ 1298 //darcs checkout 1299 $inv = file('_darcs/inventory'); 1300 $inv = preg_grep('#\*\*\d{14}[\]$]#',$inv); 1301 $cur = array_pop($inv); 1302 preg_match('#\*\*(\d{4})(\d{2})(\d{2})#',$cur,$matches); 1303 return 'Darcs '.$matches[1].'-'.$matches[2].'-'.$matches[3]; 1304 }else{ 1305 return 'snapshot?'; 1306 } 1307} 1308 1309/** 1310 * Run a few sanity checks 1311 * 1312 * @author Andreas Gohr <andi@splitbrain.org> 1313 */ 1314function check(){ 1315 global $conf; 1316 global $INFO; 1317 1318 msg('DokuWiki version: '.getVersion(),1); 1319 1320 if(version_compare(phpversion(),'4.3.0','<')){ 1321 msg('Your PHP version is too old ('.phpversion().' vs. 4.3.+ recommended)',-1); 1322 }elseif(version_compare(phpversion(),'4.3.10','<')){ 1323 msg('Consider upgrading PHP to 4.3.10 or higher for security reasons (your version: '.phpversion().')',0); 1324 }else{ 1325 msg('PHP version '.phpversion(),1); 1326 } 1327 1328 if(is_writable($conf['changelog'])){ 1329 msg('Changelog is writable',1); 1330 }else{ 1331 msg('Changelog is not writable',-1); 1332 } 1333 1334 if(is_writable($conf['datadir'])){ 1335 msg('Datadir is writable',1); 1336 }else{ 1337 msg('Datadir is not writable',-1); 1338 } 1339 1340 if(is_writable($conf['olddir'])){ 1341 msg('Attic is writable',1); 1342 }else{ 1343 msg('Attic is not writable',-1); 1344 } 1345 1346 if(is_writable($conf['mediadir'])){ 1347 msg('Mediadir is writable',1); 1348 }else{ 1349 msg('Mediadir is not writable',-1); 1350 } 1351 1352 if(is_writable($conf['cachedir'])){ 1353 msg('Cachedir is writable',1); 1354 }else{ 1355 msg('Cachedir is not writable',-1); 1356 } 1357 1358 if(is_writable($conf['lockdir'])){ 1359 msg('Lockdir is writable',1); 1360 }else{ 1361 msg('Lockdir is not writable',-1); 1362 } 1363 1364 if(is_writable(DOKU_CONF.'users.auth.php')){ 1365 msg('conf/users.auth.php is writable',1); 1366 }else{ 1367 msg('conf/users.auth.php is not writable',0); 1368 } 1369 1370 if(function_exists('mb_strpos')){ 1371 if(defined('UTF8_NOMBSTRING')){ 1372 msg('mb_string extension is available but will not be used',0); 1373 }else{ 1374 msg('mb_string extension is available and will be used',1); 1375 } 1376 }else{ 1377 msg('mb_string extension not available - PHP only replacements will be used',0); 1378 } 1379 1380 if($conf['allowdebug']){ 1381 msg('Debugging support is enabled. If you don\'t need it you should set $conf[\'allowdebug\'] = 0',-1); 1382 }else{ 1383 msg('Debugging support is disabled',1); 1384 } 1385 1386 msg('Your current permission for this page is '.$INFO['perm'],0); 1387 1388 if(is_writable($INFO['filepath'])){ 1389 msg('The current page is writable by the webserver',0); 1390 }else{ 1391 msg('The current page is not writable by the webserver',0); 1392 } 1393 1394 if($INFO['writable']){ 1395 msg('The current page is writable by you',0); 1396 }else{ 1397 msg('The current page is not writable you',0); 1398 } 1399} 1400 1401/** 1402 * Let us know if a user is tracking a page 1403 * 1404 * @author Andreas Gohr <andi@splitbrain.org> 1405 */ 1406function is_subscribed($id,$uid){ 1407 $file=metaFN($id,'.mlist'); 1408 if (@file_exists($file)) { 1409 $mlist = file($file); 1410 $pos = array_search($uid."\n",$mlist); 1411 return is_int($pos); 1412 } 1413 1414 return false; 1415} 1416 1417/** 1418 * Return a string with the email addresses of all the 1419 * users subscribed to a page 1420 * 1421 * @author Steven Danz <steven-danz@kc.rr.com> 1422 */ 1423function subscriber_addresslist($id){ 1424 global $conf; 1425 global $auth; 1426 1427 $emails = ''; 1428 1429 if (!$conf['subscribers']) return; 1430 1431 $mlist = array(); 1432 $file=metaFN($id,'.mlist'); 1433 if (file_exists($file)) { 1434 $mlist = file($file); 1435 } 1436 if(count($mlist) > 0) { 1437 foreach ($mlist as $who) { 1438 $who = rtrim($who); 1439 $info = $auth->getUserData($who); 1440 $level = auth_aclcheck($id,$who,$info['grps']); 1441 if ($level >= AUTH_READ) { 1442 if (strcasecmp($info['mail'],$conf['notify']) != 0) { 1443 if (empty($emails)) { 1444 $emails = $info['mail']; 1445 } else { 1446 $emails = "$emails,".$info['mail']; 1447 } 1448 } 1449 } 1450 } 1451 } 1452 1453 return $emails; 1454} 1455 1456/** 1457 * Removes quoting backslashes 1458 * 1459 * @author Andreas Gohr <andi@splitbrain.org> 1460 */ 1461function unslash($string,$char="'"){ 1462 return str_replace('\\'.$char,$char,$string); 1463} 1464 1465//Setup VIM: ex: et ts=2 enc=utf-8 : 1466