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