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 9if(!defined('DOKU_INC')) define('DOKU_INC',fullpath(dirname(__FILE__).'/../').'/'); 10require_once(DOKU_CONF.'dokuwiki.php'); 11require_once(DOKU_INC.'inc/io.php'); 12require_once(DOKU_INC.'inc/changelog.php'); 13require_once(DOKU_INC.'inc/utf8.php'); 14require_once(DOKU_INC.'inc/mail.php'); 15require_once(DOKU_INC.'inc/parserutils.php'); 16require_once(DOKU_INC.'inc/infoutils.php'); 17 18/** 19 * These constants are used with the recents function 20 */ 21define('RECENTS_SKIP_DELETED',2); 22define('RECENTS_SKIP_MINORS',4); 23define('RECENTS_SKIP_SUBSPACES',8); 24 25/** 26 * Wrapper around htmlspecialchars() 27 * 28 * @author Andreas Gohr <andi@splitbrain.org> 29 * @see htmlspecialchars() 30 */ 31function hsc($string){ 32 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 33} 34 35/** 36 * print a newline terminated string 37 * 38 * You can give an indention as optional parameter 39 * 40 * @author Andreas Gohr <andi@splitbrain.org> 41 */ 42function ptln($string,$indent=0){ 43 echo str_repeat(' ', $indent)."$string\n"; 44} 45 46/** 47 * strips control characters (<32) from the given string 48 * 49 * @author Andreas Gohr <andi@splitbrain.org> 50 */ 51function stripctl($string){ 52 return preg_replace('/[\x00-\x1F]+/s','',$string); 53} 54 55/** 56 * Return a secret token to be used for CSRF attack prevention 57 * 58 * @author Andreas Gohr <andi@splitbrain.org> 59 * @link http://en.wikipedia.org/wiki/Cross-site_request_forgery 60 * @link http://christ1an.blogspot.com/2007/04/preventing-csrf-efficiently.html 61 * @return string 62 */ 63function getSecurityToken(){ 64 return md5(auth_cookiesalt().session_id()); 65} 66 67/** 68 * Check the secret CSRF token 69 */ 70function checkSecurityToken($token=null){ 71 if(is_null($token)) $token = $_REQUEST['sectok']; 72 if(getSecurityToken() != $token){ 73 msg('Security Token did not match. Possible CSRF attack.',-1); 74 return false; 75 } 76 return true; 77} 78 79/** 80 * Print a hidden form field with a secret CSRF token 81 * 82 * @author Andreas Gohr <andi@splitbrain.org> 83 */ 84function formSecurityToken($print=true){ 85 $ret = '<div class="no"><input type="hidden" name="sectok" value="'.getSecurityToken().'" /></div>'."\n"; 86 if($print){ 87 echo $ret; 88 }else{ 89 return $ret; 90 } 91} 92 93/** 94 * Return info about the current document as associative 95 * array. 96 * 97 * @author Andreas Gohr <andi@splitbrain.org> 98 */ 99function pageinfo(){ 100 global $ID; 101 global $REV; 102 global $USERINFO; 103 global $conf; 104 105 // include ID & REV not redundant, as some parts of DokuWiki may temporarily change $ID, e.g. p_wiki_xhtml 106 // FIXME ... perhaps it would be better to ensure the temporary changes weren't necessary 107 $info['id'] = $ID; 108 $info['rev'] = $REV; 109 110 if($_SERVER['REMOTE_USER']){ 111 $info['userinfo'] = $USERINFO; 112 $info['perm'] = auth_quickaclcheck($ID); 113 $info['subscribed'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],false); 114 $info['subscribedns'] = is_subscribed($ID,$_SERVER['REMOTE_USER'],true); 115 $info['client'] = $_SERVER['REMOTE_USER']; 116 117 // set info about manager/admin status 118 $info['isadmin'] = false; 119 $info['ismanager'] = false; 120 if($info['perm'] == AUTH_ADMIN){ 121 $info['isadmin'] = true; 122 $info['ismanager'] = true; 123 }elseif(auth_ismanager()){ 124 $info['ismanager'] = true; 125 } 126 127 // if some outside auth were used only REMOTE_USER is set 128 if(!$info['userinfo']['name']){ 129 $info['userinfo']['name'] = $_SERVER['REMOTE_USER']; 130 } 131 132 }else{ 133 $info['perm'] = auth_aclcheck($ID,'',null); 134 $info['subscribed'] = false; 135 $info['client'] = clientIP(true); 136 } 137 138 $info['namespace'] = getNS($ID); 139 $info['locked'] = checklock($ID); 140 $info['filepath'] = fullpath(wikiFN($ID)); 141 $info['exists'] = @file_exists($info['filepath']); 142 if($REV){ 143 //check if current revision was meant 144 if($info['exists'] && (@filemtime($info['filepath'])==$REV)){ 145 $REV = ''; 146 }else{ 147 //really use old revision 148 $info['filepath'] = fullpath(wikiFN($ID,$REV)); 149 $info['exists'] = @file_exists($info['filepath']); 150 } 151 } 152 $info['rev'] = $REV; 153 if($info['exists']){ 154 $info['writable'] = (is_writable($info['filepath']) && 155 ($info['perm'] >= AUTH_EDIT)); 156 }else{ 157 $info['writable'] = ($info['perm'] >= AUTH_CREATE); 158 } 159 $info['editable'] = ($info['writable'] && empty($info['lock'])); 160 $info['lastmod'] = @filemtime($info['filepath']); 161 162 //load page meta data 163 $info['meta'] = p_get_metadata($ID); 164 165 //who's the editor 166 if($REV){ 167 $revinfo = getRevisionInfo($ID, $REV, 1024); 168 }else{ 169 if (is_array($info['meta']['last_change'])) { 170 $revinfo = $info['meta']['last_change']; 171 } else { 172 $revinfo = getRevisionInfo($ID, $info['lastmod'], 1024); 173 // cache most recent changelog line in metadata if missing and still valid 174 if ($revinfo!==false) { 175 $info['meta']['last_change'] = $revinfo; 176 p_set_metadata($ID, array('last_change' => $revinfo)); 177 } 178 } 179 } 180 //and check for an external edit 181 if($revinfo!==false && $revinfo['date']!=$info['lastmod']){ 182 // cached changelog line no longer valid 183 $revinfo = false; 184 $info['meta']['last_change'] = $revinfo; 185 p_set_metadata($ID, array('last_change' => $revinfo)); 186 } 187 188 $info['ip'] = $revinfo['ip']; 189 $info['user'] = $revinfo['user']; 190 $info['sum'] = $revinfo['sum']; 191 // See also $INFO['meta']['last_change'] which is the most recent log line for page $ID. 192 // Use $INFO['meta']['last_change']['type']===DOKU_CHANGE_TYPE_MINOR_EDIT in place of $info['minor']. 193 194 if($revinfo['user']){ 195 $info['editor'] = $revinfo['user']; 196 }else{ 197 $info['editor'] = $revinfo['ip']; 198 } 199 200 // draft 201 $draft = getCacheName($info['client'].$ID,'.draft'); 202 if(@file_exists($draft)){ 203 if(@filemtime($draft) < @filemtime(wikiFN($ID))){ 204 // remove stale draft 205 @unlink($draft); 206 }else{ 207 $info['draft'] = $draft; 208 } 209 } 210 211 return $info; 212} 213 214/** 215 * Build an string of URL parameters 216 * 217 * @author Andreas Gohr 218 */ 219function buildURLparams($params, $sep='&'){ 220 $url = ''; 221 $amp = false; 222 foreach($params as $key => $val){ 223 if($amp) $url .= $sep; 224 225 $url .= $key.'='; 226 $url .= rawurlencode($val); 227 $amp = true; 228 } 229 return $url; 230} 231 232/** 233 * Build an string of html tag attributes 234 * 235 * Skips keys starting with '_', values get HTML encoded 236 * 237 * @author Andreas Gohr 238 */ 239function buildAttributes($params,$skipempty=false){ 240 $url = ''; 241 foreach($params as $key => $val){ 242 if($key{0} == '_') continue; 243 if($val === '' && $skipempty) continue; 244 245 $url .= $key.'="'; 246 $url .= htmlspecialchars ($val); 247 $url .= '" '; 248 } 249 return $url; 250} 251 252 253/** 254 * This builds the breadcrumb trail and returns it as array 255 * 256 * @author Andreas Gohr <andi@splitbrain.org> 257 */ 258function breadcrumbs(){ 259 // we prepare the breadcrumbs early for quick session closing 260 static $crumbs = null; 261 if($crumbs != null) return $crumbs; 262 263 global $ID; 264 global $ACT; 265 global $conf; 266 $crumbs = $_SESSION[DOKU_COOKIE]['bc']; 267 268 //first visit? 269 if (!is_array($crumbs)){ 270 $crumbs = array(); 271 } 272 //we only save on show and existing wiki documents 273 $file = wikiFN($ID); 274 if($ACT != 'show' || !@file_exists($file)){ 275 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 276 return $crumbs; 277 } 278 279 // page names 280 $name = noNSorNS($ID); 281 if ($conf['useheading']) { 282 // get page title 283 $title = p_get_first_heading($ID,true); 284 if ($title) { 285 $name = $title; 286 } 287 } 288 289 //remove ID from array 290 if (isset($crumbs[$ID])) { 291 unset($crumbs[$ID]); 292 } 293 294 //add to array 295 $crumbs[$ID] = $name; 296 //reduce size 297 while(count($crumbs) > $conf['breadcrumbs']){ 298 array_shift($crumbs); 299 } 300 //save to session 301 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 302 return $crumbs; 303} 304 305/** 306 * Filter for page IDs 307 * 308 * This is run on a ID before it is outputted somewhere 309 * currently used to replace the colon with something else 310 * on Windows systems and to have proper URL encoding 311 * 312 * Urlencoding is ommitted when the second parameter is false 313 * 314 * @author Andreas Gohr <andi@splitbrain.org> 315 */ 316function idfilter($id,$ue=true){ 317 global $conf; 318 if ($conf['useslash'] && $conf['userewrite']){ 319 $id = strtr($id,':','/'); 320 }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && 321 $conf['userewrite']) { 322 $id = strtr($id,':',';'); 323 } 324 if($ue){ 325 $id = rawurlencode($id); 326 $id = str_replace('%3A',':',$id); //keep as colon 327 $id = str_replace('%2F','/',$id); //keep as slash 328 } 329 return $id; 330} 331 332/** 333 * This builds a link to a wikipage 334 * 335 * It handles URL rewriting and adds additional parameter if 336 * given in $more 337 * 338 * @author Andreas Gohr <andi@splitbrain.org> 339 */ 340function wl($id='',$more='',$abs=false,$sep='&'){ 341 global $conf; 342 if(is_array($more)){ 343 $more = buildURLparams($more,$sep); 344 }else{ 345 $more = str_replace(',',$sep,$more); 346 } 347 348 $id = idfilter($id); 349 if($abs){ 350 $xlink = DOKU_URL; 351 }else{ 352 $xlink = DOKU_BASE; 353 } 354 355 if($conf['userewrite'] == 2){ 356 $xlink .= DOKU_SCRIPT.'/'.$id; 357 if($more) $xlink .= '?'.$more; 358 }elseif($conf['userewrite']){ 359 $xlink .= $id; 360 if($more) $xlink .= '?'.$more; 361 }elseif($id){ 362 $xlink .= DOKU_SCRIPT.'?id='.$id; 363 if($more) $xlink .= $sep.$more; 364 }else{ 365 $xlink .= DOKU_SCRIPT; 366 if($more) $xlink .= '?'.$more; 367 } 368 369 return $xlink; 370} 371 372/** 373 * This builds a link to an alternate page format 374 * 375 * Handles URL rewriting if enabled. Follows the style of wl(). 376 * 377 * @author Ben Coburn <btcoburn@silicodon.net> 378 */ 379function exportlink($id='',$format='raw',$more='',$abs=false,$sep='&'){ 380 global $conf; 381 if(is_array($more)){ 382 $more = buildURLparams($more,$sep); 383 }else{ 384 $more = str_replace(',',$sep,$more); 385 } 386 387 $format = rawurlencode($format); 388 $id = idfilter($id); 389 if($abs){ 390 $xlink = DOKU_URL; 391 }else{ 392 $xlink = DOKU_BASE; 393 } 394 395 if($conf['userewrite'] == 2){ 396 $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format; 397 if($more) $xlink .= $sep.$more; 398 }elseif($conf['userewrite'] == 1){ 399 $xlink .= '_export/'.$format.'/'.$id; 400 if($more) $xlink .= '?'.$more; 401 }else{ 402 $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id; 403 if($more) $xlink .= $sep.$more; 404 } 405 406 return $xlink; 407} 408 409/** 410 * Build a link to a media file 411 * 412 * Will return a link to the detail page if $direct is false 413 */ 414function ml($id='',$more='',$direct=true,$sep='&',$abs=false){ 415 global $conf; 416 if(is_array($more)){ 417 $more = buildURLparams($more,$sep); 418 }else{ 419 $more = str_replace(',',$sep,$more); 420 } 421 422 if($abs){ 423 $xlink = DOKU_URL; 424 }else{ 425 $xlink = DOKU_BASE; 426 } 427 428 // external URLs are always direct without rewriting 429 if(preg_match('#^(https?|ftp)://#i',$id)){ 430 $xlink .= 'lib/exe/fetch.php'; 431 if($more){ 432 $xlink .= '?'.$more; 433 $xlink .= $sep.'media='.rawurlencode($id); 434 }else{ 435 $xlink .= '?media='.rawurlencode($id); 436 } 437 return $xlink; 438 } 439 440 $id = idfilter($id); 441 442 // decide on scriptname 443 if($direct){ 444 if($conf['userewrite'] == 1){ 445 $script = '_media'; 446 }else{ 447 $script = 'lib/exe/fetch.php'; 448 } 449 }else{ 450 if($conf['userewrite'] == 1){ 451 $script = '_detail'; 452 }else{ 453 $script = 'lib/exe/detail.php'; 454 } 455 } 456 457 // build URL based on rewrite mode 458 if($conf['userewrite']){ 459 $xlink .= $script.'/'.$id; 460 if($more) $xlink .= '?'.$more; 461 }else{ 462 if($more){ 463 $xlink .= $script.'?'.$more; 464 $xlink .= $sep.'media='.$id; 465 }else{ 466 $xlink .= $script.'?media='.$id; 467 } 468 } 469 470 return $xlink; 471} 472 473 474 475/** 476 * Just builds a link to a script 477 * 478 * @todo maybe obsolete 479 * @author Andreas Gohr <andi@splitbrain.org> 480 */ 481function script($script='doku.php'){ 482# $link = getBaseURL(); 483# $link .= $script; 484# return $link; 485 return DOKU_BASE.DOKU_SCRIPT; 486} 487 488/** 489 * Spamcheck against wordlist 490 * 491 * Checks the wikitext against a list of blocked expressions 492 * returns true if the text contains any bad words 493 * 494 * @author Andreas Gohr <andi@splitbrain.org> 495 */ 496function checkwordblock(){ 497 global $TEXT; 498 global $conf; 499 500 if(!$conf['usewordblock']) return false; 501 502 // we prepare the text a tiny bit to prevent spammers circumventing URL checks 503 $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT); 504 505 $wordblocks = getWordblocks(); 506 //how many lines to read at once (to work around some PCRE limits) 507 if(version_compare(phpversion(),'4.3.0','<')){ 508 //old versions of PCRE define a maximum of parenthesises even if no 509 //backreferences are used - the maximum is 99 510 //this is very bad performancewise and may even be too high still 511 $chunksize = 40; 512 }else{ 513 //read file in chunks of 200 - this should work around the 514 //MAX_PATTERN_SIZE in modern PCRE 515 $chunksize = 200; 516 } 517 while($blocks = array_splice($wordblocks,0,$chunksize)){ 518 $re = array(); 519 #build regexp from blocks 520 foreach($blocks as $block){ 521 $block = preg_replace('/#.*$/','',$block); 522 $block = trim($block); 523 if(empty($block)) continue; 524 $re[] = $block; 525 } 526 if(count($re) && preg_match('#('.join('|',$re).')#si',$text)) { 527 return true; 528 } 529 } 530 return false; 531} 532 533/** 534 * Return the IP of the client 535 * 536 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 537 * 538 * It returns a comma separated list of IPs if the above mentioned 539 * headers are set. If the single parameter is set, it tries to return 540 * a routable public address, prefering the ones suplied in the X 541 * headers 542 * 543 * @param boolean $single If set only a single IP is returned 544 * @author Andreas Gohr <andi@splitbrain.org> 545 */ 546function clientIP($single=false){ 547 $ip = array(); 548 $ip[] = $_SERVER['REMOTE_ADDR']; 549 if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) 550 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR'])); 551 if(!empty($_SERVER['HTTP_X_REAL_IP'])) 552 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP'])); 553 554 // remove any non-IP stuff 555 $cnt = count($ip); 556 $match = array(); 557 for($i=0; $i<$cnt; $i++){ 558 if(preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i],$match)) { 559 $ip[$i] = $match[0]; 560 } else { 561 $ip[$i] = ''; 562 } 563 if(empty($ip[$i])) unset($ip[$i]); 564 } 565 $ip = array_values(array_unique($ip)); 566 if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 567 568 if(!$single) return join(',',$ip); 569 570 // decide which IP to use, trying to avoid local addresses 571 $ip = array_reverse($ip); 572 foreach($ip as $i){ 573 if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){ 574 continue; 575 }else{ 576 return $i; 577 } 578 } 579 // still here? just use the first (last) address 580 return $ip[0]; 581} 582 583/** 584 * Convert one or more comma separated IPs to hostnames 585 * 586 * @author Glen Harris <astfgl@iamnota.org> 587 * @returns a comma separated list of hostnames 588 */ 589function gethostsbyaddrs($ips){ 590 $hosts = array(); 591 $ips = explode(',',$ips); 592 593 if(is_array($ips)) { 594 foreach($ips as $ip){ 595 $hosts[] = gethostbyaddr(trim($ip)); 596 } 597 return join(',',$hosts); 598 } else { 599 return gethostbyaddr(trim($ips)); 600 } 601} 602 603/** 604 * Checks if a given page is currently locked. 605 * 606 * removes stale lockfiles 607 * 608 * @author Andreas Gohr <andi@splitbrain.org> 609 */ 610function checklock($id){ 611 global $conf; 612 $lock = wikiLockFN($id); 613 614 //no lockfile 615 if(!@file_exists($lock)) return false; 616 617 //lockfile expired 618 if((time() - filemtime($lock)) > $conf['locktime']){ 619 @unlink($lock); 620 return false; 621 } 622 623 //my own lock 624 $ip = io_readFile($lock); 625 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 626 return false; 627 } 628 629 return $ip; 630} 631 632/** 633 * Lock a page for editing 634 * 635 * @author Andreas Gohr <andi@splitbrain.org> 636 */ 637function lock($id){ 638 $lock = wikiLockFN($id); 639 if($_SERVER['REMOTE_USER']){ 640 io_saveFile($lock,$_SERVER['REMOTE_USER']); 641 }else{ 642 io_saveFile($lock,clientIP()); 643 } 644} 645 646/** 647 * Unlock a page if it was locked by the user 648 * 649 * @author Andreas Gohr <andi@splitbrain.org> 650 * @return bool true if a lock was removed 651 */ 652function unlock($id){ 653 $lock = wikiLockFN($id); 654 if(@file_exists($lock)){ 655 $ip = io_readFile($lock); 656 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 657 @unlink($lock); 658 return true; 659 } 660 } 661 return false; 662} 663 664/** 665 * convert line ending to unix format 666 * 667 * @see formText() for 2crlf conversion 668 * @author Andreas Gohr <andi@splitbrain.org> 669 */ 670function cleanText($text){ 671 $text = preg_replace("/(\015\012)|(\015)/","\012",$text); 672 return $text; 673} 674 675/** 676 * Prepares text for print in Webforms by encoding special chars. 677 * It also converts line endings to Windows format which is 678 * pseudo standard for webforms. 679 * 680 * @see cleanText() for 2unix conversion 681 * @author Andreas Gohr <andi@splitbrain.org> 682 */ 683function formText($text){ 684 $text = str_replace("\012","\015\012",$text); 685 return htmlspecialchars($text); 686} 687 688/** 689 * Returns the specified local text in raw format 690 * 691 * @author Andreas Gohr <andi@splitbrain.org> 692 */ 693function rawLocale($id){ 694 return io_readFile(localeFN($id)); 695} 696 697/** 698 * Returns the raw WikiText 699 * 700 * @author Andreas Gohr <andi@splitbrain.org> 701 */ 702function rawWiki($id,$rev=''){ 703 return io_readWikiPage(wikiFN($id, $rev), $id, $rev); 704} 705 706/** 707 * Returns the pagetemplate contents for the ID's namespace 708 * 709 * @author Andreas Gohr <andi@splitbrain.org> 710 */ 711function pageTemplate($data){ 712 $id = $data[0]; 713 global $conf; 714 global $INFO; 715 716 $path = dirname(wikiFN($id)); 717 718 if(@file_exists($path.'/_template.txt')){ 719 $tpl = io_readFile($path.'/_template.txt'); 720 }else{ 721 // search upper namespaces for templates 722 $len = strlen(rtrim($conf['datadir'],'/')); 723 while (strlen($path) >= $len){ 724 if(@file_exists($path.'/__template.txt')){ 725 $tpl = io_readFile($path.'/__template.txt'); 726 break; 727 } 728 $path = substr($path, 0, strrpos($path, '/')); 729 } 730 } 731 if(!$tpl) return ''; 732 733 // replace placeholders 734 $tpl = str_replace('@ID@',$id,$tpl); 735 $tpl = str_replace('@NS@',getNS($id),$tpl); 736 $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl); 737 $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl); 738 $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl); 739 $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl); 740 $tpl = str_replace('@DATE@',$conf['dformat'],$tpl); 741 // we need the callback to work around strftime's char limit 742 $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl); 743 744 return $tpl; 745} 746 747 748/** 749 * Returns the raw Wiki Text in three slices. 750 * 751 * The range parameter needs to have the form "from-to" 752 * and gives the range of the section in bytes - no 753 * UTF-8 awareness is needed. 754 * The returned order is prefix, section and suffix. 755 * 756 * @author Andreas Gohr <andi@splitbrain.org> 757 */ 758function rawWikiSlices($range,$id,$rev=''){ 759 list($from,$to) = split('-',$range,2); 760 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev); 761 if(!$from) $from = 0; 762 if(!$to) $to = strlen($text)+1; 763 764 $slices[0] = substr($text,0,$from-1); 765 $slices[1] = substr($text,$from-1,$to-$from); 766 $slices[2] = substr($text,$to); 767 768 return $slices; 769} 770 771/** 772 * Joins wiki text slices 773 * 774 * function to join the text slices with correct lineendings again. 775 * When the pretty parameter is set to true it adds additional empty 776 * lines between sections if needed (used on saving). 777 * 778 * @author Andreas Gohr <andi@splitbrain.org> 779 */ 780function con($pre,$text,$suf,$pretty=false){ 781 782 if($pretty){ 783 if($pre && substr($pre,-1) != "\n") $pre .= "\n"; 784 if($suf && substr($text,-1) != "\n") $text .= "\n"; 785 } 786 787 if($pre) $pre .= "\n"; 788 if($suf) $text .= "\n"; 789 return $pre.$text.$suf; 790} 791 792/** 793 * Saves a wikitext by calling io_writeWikiPage. 794 * Also directs changelog and attic updates. 795 * 796 * @author Andreas Gohr <andi@splitbrain.org> 797 * @author Ben Coburn <btcoburn@silicodon.net> 798 */ 799function saveWikiText($id,$text,$summary,$minor=false){ 800 /* Note to developers: 801 This code is subtle and delicate. Test the behavior of 802 the attic and changelog with dokuwiki and external edits 803 after any changes. External edits change the wiki page 804 directly without using php or dokuwiki. 805 */ 806 global $conf; 807 global $lang; 808 global $REV; 809 // ignore if no changes were made 810 if($text == rawWiki($id,'')){ 811 return; 812 } 813 814 $file = wikiFN($id); 815 $old = @filemtime($file); // from page 816 $wasRemoved = empty($text); 817 $wasCreated = !@file_exists($file); 818 $wasReverted = ($REV==true); 819 $newRev = false; 820 $oldRev = getRevisions($id, -1, 1, 1024); // from changelog 821 $oldRev = (int)(empty($oldRev)?0:$oldRev[0]); 822 if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) { 823 // add old revision to the attic if missing 824 saveOldRevision($id); 825 // add a changelog entry if this edit came from outside dokuwiki 826 if ($old>$oldRev) { 827 addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true)); 828 // remove soon to be stale instructions 829 $cache = new cache_instructions($id, $file); 830 $cache->removeCache(); 831 } 832 } 833 834 if ($wasRemoved){ 835 // Send "update" event with empty data, so plugins can react to page deletion 836 $data = array(array($file, '', false), getNS($id), noNS($id), false); 837 trigger_event('IO_WIKIPAGE_WRITE', $data); 838 // pre-save deleted revision 839 @touch($file); 840 clearstatcache(); 841 $newRev = saveOldRevision($id); 842 // remove empty file 843 @unlink($file); 844 // remove old meta info... 845 $mfiles = metaFiles($id); 846 $changelog = metaFN($id, '.changes'); 847 $metadata = metaFN($id, '.meta'); 848 foreach ($mfiles as $mfile) { 849 // but keep per-page changelog to preserve page history and keep meta data 850 if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); } 851 } 852 // purge meta data 853 p_purge_metadata($id); 854 $del = true; 855 // autoset summary on deletion 856 if(empty($summary)) $summary = $lang['deleted']; 857 // remove empty namespaces 858 io_sweepNS($id, 'datadir'); 859 io_sweepNS($id, 'mediadir'); 860 }else{ 861 // save file (namespace dir is created in io_writeWikiPage) 862 io_writeWikiPage($file, $text, $id); 863 // pre-save the revision, to keep the attic in sync 864 $newRev = saveOldRevision($id); 865 $del = false; 866 } 867 868 // select changelog line type 869 $extra = ''; 870 $type = DOKU_CHANGE_TYPE_EDIT; 871 if ($wasReverted) { 872 $type = DOKU_CHANGE_TYPE_REVERT; 873 $extra = $REV; 874 } 875 else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; } 876 else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; } 877 else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users 878 879 addLogEntry($newRev, $id, $type, $summary, $extra); 880 // send notify mails 881 notify($id,'admin',$old,$summary,$minor); 882 notify($id,'subscribers',$old,$summary,$minor); 883 884 // update the purgefile (timestamp of the last time anything within the wiki was changed) 885 io_saveFile($conf['cachedir'].'/purgefile',time()); 886} 887 888/** 889 * moves the current version to the attic and returns its 890 * revision date 891 * 892 * @author Andreas Gohr <andi@splitbrain.org> 893 */ 894function saveOldRevision($id){ 895 global $conf; 896 $oldf = wikiFN($id); 897 if(!@file_exists($oldf)) return ''; 898 $date = filemtime($oldf); 899 $newf = wikiFN($id,$date); 900 io_writeWikiPage($newf, rawWiki($id), $id, $date); 901 return $date; 902} 903 904/** 905 * Sends a notify mail on page change 906 * 907 * @param string $id The changed page 908 * @param string $who Who to notify (admin|subscribers) 909 * @param int $rev Old page revision 910 * @param string $summary What changed 911 * @param boolean $minor Is this a minor edit? 912 * @param array $replace Additional string substitutions, @KEY@ to be replaced by value 913 * 914 * @author Andreas Gohr <andi@splitbrain.org> 915 */ 916function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ 917 global $lang; 918 global $conf; 919 global $INFO; 920 921 // decide if there is something to do 922 if($who == 'admin'){ 923 if(empty($conf['notify'])) return; //notify enabled? 924 $text = rawLocale('mailtext'); 925 $to = $conf['notify']; 926 $bcc = ''; 927 }elseif($who == 'subscribers'){ 928 if(!$conf['subscribers']) return; //subscribers enabled? 929 if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors 930 $bcc = subscriber_addresslist($id); 931 if(empty($bcc)) return; 932 $to = ''; 933 $text = rawLocale('subscribermail'); 934 }elseif($who == 'register'){ 935 if(empty($conf['registernotify'])) return; 936 $text = rawLocale('registermail'); 937 $to = $conf['registernotify']; 938 $bcc = ''; 939 }else{ 940 return; //just to be safe 941 } 942 943 $ip = clientIP(); 944 $text = str_replace('@DATE@',strftime($conf['dformat']),$text); 945 $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); 946 $text = str_replace('@IPADDRESS@',$ip,$text); 947 $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text); 948 $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text); 949 $text = str_replace('@PAGE@',$id,$text); 950 $text = str_replace('@TITLE@',$conf['title'],$text); 951 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 952 $text = str_replace('@SUMMARY@',$summary,$text); 953 $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); 954 955 foreach ($replace as $key => $substitution) { 956 $text = str_replace('@'.strtoupper($key).'@',$substitution, $text); 957 } 958 959 if($who == 'register'){ 960 $subject = $lang['mail_new_user'].' '.$summary; 961 }elseif($rev){ 962 $subject = $lang['mail_changed'].' '.$id; 963 $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text); 964 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 965 $df = new Diff(split("\n",rawWiki($id,$rev)), 966 split("\n",rawWiki($id))); 967 $dformat = new UnifiedDiffFormatter(); 968 $diff = $dformat->format($df); 969 }else{ 970 $subject=$lang['mail_newpage'].' '.$id; 971 $text = str_replace('@OLDPAGE@','none',$text); 972 $diff = rawWiki($id); 973 } 974 $text = str_replace('@DIFF@',$diff,$text); 975 $subject = '['.$conf['title'].'] '.$subject; 976 977 $from = $conf['mailfrom']; 978 $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from); 979 $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from); 980 $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from); 981 982 mail_send($to,$subject,$text,$from,'',$bcc); 983} 984 985/** 986 * extracts the query from a search engine referrer 987 * 988 * @author Andreas Gohr <andi@splitbrain.org> 989 * @author Todd Augsburger <todd@rollerorgans.com> 990 */ 991function getGoogleQuery(){ 992 $url = parse_url($_SERVER['HTTP_REFERER']); 993 if(!$url) return ''; 994 995 $query = array(); 996 parse_str($url['query'],$query); 997 if(isset($query['q'])) 998 $q = $query['q']; // google, live/msn, aol, ask, altavista, alltheweb, gigablast 999 elseif(isset($query['p'])) 1000 $q = $query['p']; // yahoo 1001 elseif(isset($query['query'])) 1002 $q = $query['query']; // lycos, netscape, clusty, hotbot 1003 elseif(preg_match("#a9\.com#i",$url['host'])) // a9 1004 $q = urldecode(ltrim($url['path'],'/')); 1005 1006 if(!$q) return ''; 1007 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY); 1008 return $q; 1009} 1010 1011/** 1012 * Try to set correct locale 1013 * 1014 * @deprecated No longer used 1015 * @author Andreas Gohr <andi@splitbrain.org> 1016 */ 1017function setCorrectLocale(){ 1018 global $conf; 1019 global $lang; 1020 1021 $enc = strtoupper($lang['encoding']); 1022 foreach ($lang['locales'] as $loc){ 1023 //try locale 1024 if(@setlocale(LC_ALL,$loc)) return; 1025 //try loceale with encoding 1026 if(@setlocale(LC_ALL,"$loc.$enc")) return; 1027 } 1028 //still here? try to set from environment 1029 @setlocale(LC_ALL,""); 1030} 1031 1032/** 1033 * Return the human readable size of a file 1034 * 1035 * @param int $size A file size 1036 * @param int $dec A number of decimal places 1037 * @author Martin Benjamin <b.martin@cybernet.ch> 1038 * @author Aidan Lister <aidan@php.net> 1039 * @version 1.0.0 1040 */ 1041function filesize_h($size, $dec = 1){ 1042 $sizes = array('B', 'KB', 'MB', 'GB'); 1043 $count = count($sizes); 1044 $i = 0; 1045 1046 while ($size >= 1024 && ($i < $count - 1)) { 1047 $size /= 1024; 1048 $i++; 1049 } 1050 1051 return round($size, $dec) . ' ' . $sizes[$i]; 1052} 1053 1054/** 1055 * return an obfuscated email address in line with $conf['mailguard'] setting 1056 * 1057 * @author Harry Fuecks <hfuecks@gmail.com> 1058 * @author Christopher Smith <chris@jalakai.co.uk> 1059 */ 1060function obfuscate($email) { 1061 global $conf; 1062 1063 switch ($conf['mailguard']) { 1064 case 'visible' : 1065 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1066 return strtr($email, $obfuscate); 1067 1068 case 'hex' : 1069 $encode = ''; 1070 for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';'; 1071 return $encode; 1072 1073 case 'none' : 1074 default : 1075 return $email; 1076 } 1077} 1078 1079/** 1080 * Let us know if a user is tracking a page or a namespace 1081 * 1082 * @author Andreas Gohr <andi@splitbrain.org> 1083 */ 1084function is_subscribed($id,$uid,$ns=false){ 1085 if(!$ns) { 1086 $file=metaFN($id,'.mlist'); 1087 } else { 1088 if(!getNS($id)) { 1089 $file = metaFN(getNS($id),'.mlist'); 1090 } else { 1091 $file = metaFN(getNS($id),'/.mlist'); 1092 } 1093 } 1094 if (@file_exists($file)) { 1095 $mlist = file($file); 1096 $pos = array_search($uid."\n",$mlist); 1097 return is_int($pos); 1098 } 1099 1100 return false; 1101} 1102 1103/** 1104 * Return a string with the email addresses of all the 1105 * users subscribed to a page 1106 * 1107 * @author Steven Danz <steven-danz@kc.rr.com> 1108 */ 1109function subscriber_addresslist($id){ 1110 global $conf; 1111 global $auth; 1112 1113 if (!$conf['subscribers']) return ''; 1114 1115 $users = array(); 1116 $emails = array(); 1117 1118 // load the page mlist file content 1119 $mlist = array(); 1120 $file=metaFN($id,'.mlist'); 1121 if (@file_exists($file)) { 1122 $mlist = file($file); 1123 foreach ($mlist as $who) { 1124 $who = rtrim($who); 1125 $users[$who] = true; 1126 } 1127 } 1128 1129 // load also the namespace mlist file content 1130 $ns = getNS($id); 1131 while ($ns) { 1132 $nsfile = metaFN($ns,'/.mlist'); 1133 if (@file_exists($nsfile)) { 1134 $mlist = file($nsfile); 1135 foreach ($mlist as $who) { 1136 $who = rtrim($who); 1137 $users[$who] = true; 1138 } 1139 } 1140 $ns = getNS($ns); 1141 } 1142 // root namespace 1143 $nsfile = metaFN('','.mlist'); 1144 if (@file_exists($nsfile)) { 1145 $mlist = file($nsfile); 1146 foreach ($mlist as $who) { 1147 $who = rtrim($who); 1148 $users[$who] = true; 1149 } 1150 } 1151 if(!empty($users)) { 1152 foreach (array_keys($users) as $who) { 1153 $info = $auth->getUserData($who); 1154 if($info === false) continue; 1155 $level = auth_aclcheck($id,$who,$info['grps']); 1156 if ($level >= AUTH_READ) { 1157 if (strcasecmp($info['mail'],$conf['notify']) != 0) { 1158 $emails[] = $info['mail']; 1159 } 1160 } 1161 } 1162 } 1163 1164 return implode(',',$emails); 1165} 1166 1167/** 1168 * Removes quoting backslashes 1169 * 1170 * @author Andreas Gohr <andi@splitbrain.org> 1171 */ 1172function unslash($string,$char="'"){ 1173 return str_replace('\\'.$char,$char,$string); 1174} 1175 1176/** 1177 * Convert php.ini shorthands to byte 1178 * 1179 * @author <gilthans dot NO dot SPAM at gmail dot com> 1180 * @link http://de3.php.net/manual/en/ini.core.php#79564 1181 */ 1182function php_to_byte($v){ 1183 $l = substr($v, -1); 1184 $ret = substr($v, 0, -1); 1185 switch(strtoupper($l)){ 1186 case 'P': 1187 $ret *= 1024; 1188 case 'T': 1189 $ret *= 1024; 1190 case 'G': 1191 $ret *= 1024; 1192 case 'M': 1193 $ret *= 1024; 1194 case 'K': 1195 $ret *= 1024; 1196 break; 1197 } 1198 return $ret; 1199} 1200 1201/** 1202 * Wrapper around preg_quote adding the default delimiter 1203 */ 1204function preg_quote_cb($string){ 1205 return preg_quote($string,'/'); 1206} 1207 1208//Setup VIM: ex: et ts=2 enc=utf-8 : 1209