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 // Avoid double newline above section when saving section edit 788 //if($pre) $pre .= "\n"; 789 if($suf) $text .= "\n"; 790 return $pre.$text.$suf; 791} 792 793/** 794 * Saves a wikitext by calling io_writeWikiPage. 795 * Also directs changelog and attic updates. 796 * 797 * @author Andreas Gohr <andi@splitbrain.org> 798 * @author Ben Coburn <btcoburn@silicodon.net> 799 */ 800function saveWikiText($id,$text,$summary,$minor=false){ 801 /* Note to developers: 802 This code is subtle and delicate. Test the behavior of 803 the attic and changelog with dokuwiki and external edits 804 after any changes. External edits change the wiki page 805 directly without using php or dokuwiki. 806 */ 807 global $conf; 808 global $lang; 809 global $REV; 810 // ignore if no changes were made 811 if($text == rawWiki($id,'')){ 812 return; 813 } 814 815 $file = wikiFN($id); 816 $old = @filemtime($file); // from page 817 $wasRemoved = empty($text); 818 $wasCreated = !@file_exists($file); 819 $wasReverted = ($REV==true); 820 $newRev = false; 821 $oldRev = getRevisions($id, -1, 1, 1024); // from changelog 822 $oldRev = (int)(empty($oldRev)?0:$oldRev[0]); 823 if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) { 824 // add old revision to the attic if missing 825 saveOldRevision($id); 826 // add a changelog entry if this edit came from outside dokuwiki 827 if ($old>$oldRev) { 828 addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true)); 829 // remove soon to be stale instructions 830 $cache = new cache_instructions($id, $file); 831 $cache->removeCache(); 832 } 833 } 834 835 if ($wasRemoved){ 836 // Send "update" event with empty data, so plugins can react to page deletion 837 $data = array(array($file, '', false), getNS($id), noNS($id), false); 838 trigger_event('IO_WIKIPAGE_WRITE', $data); 839 // pre-save deleted revision 840 @touch($file); 841 clearstatcache(); 842 $newRev = saveOldRevision($id); 843 // remove empty file 844 @unlink($file); 845 // remove old meta info... 846 $mfiles = metaFiles($id); 847 $changelog = metaFN($id, '.changes'); 848 $metadata = metaFN($id, '.meta'); 849 foreach ($mfiles as $mfile) { 850 // but keep per-page changelog to preserve page history and keep meta data 851 if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); } 852 } 853 // purge meta data 854 p_purge_metadata($id); 855 $del = true; 856 // autoset summary on deletion 857 if(empty($summary)) $summary = $lang['deleted']; 858 // remove empty namespaces 859 io_sweepNS($id, 'datadir'); 860 io_sweepNS($id, 'mediadir'); 861 }else{ 862 // save file (namespace dir is created in io_writeWikiPage) 863 io_writeWikiPage($file, $text, $id); 864 // pre-save the revision, to keep the attic in sync 865 $newRev = saveOldRevision($id); 866 $del = false; 867 } 868 869 // select changelog line type 870 $extra = ''; 871 $type = DOKU_CHANGE_TYPE_EDIT; 872 if ($wasReverted) { 873 $type = DOKU_CHANGE_TYPE_REVERT; 874 $extra = $REV; 875 } 876 else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; } 877 else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; } 878 else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users 879 880 addLogEntry($newRev, $id, $type, $summary, $extra); 881 // send notify mails 882 notify($id,'admin',$old,$summary,$minor); 883 notify($id,'subscribers',$old,$summary,$minor); 884 885 // update the purgefile (timestamp of the last time anything within the wiki was changed) 886 io_saveFile($conf['cachedir'].'/purgefile',time()); 887} 888 889/** 890 * moves the current version to the attic and returns its 891 * revision date 892 * 893 * @author Andreas Gohr <andi@splitbrain.org> 894 */ 895function saveOldRevision($id){ 896 global $conf; 897 $oldf = wikiFN($id); 898 if(!@file_exists($oldf)) return ''; 899 $date = filemtime($oldf); 900 $newf = wikiFN($id,$date); 901 io_writeWikiPage($newf, rawWiki($id), $id, $date); 902 return $date; 903} 904 905/** 906 * Sends a notify mail on page change 907 * 908 * @param string $id The changed page 909 * @param string $who Who to notify (admin|subscribers) 910 * @param int $rev Old page revision 911 * @param string $summary What changed 912 * @param boolean $minor Is this a minor edit? 913 * @param array $replace Additional string substitutions, @KEY@ to be replaced by value 914 * 915 * @author Andreas Gohr <andi@splitbrain.org> 916 */ 917function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ 918 global $lang; 919 global $conf; 920 global $INFO; 921 922 // decide if there is something to do 923 if($who == 'admin'){ 924 if(empty($conf['notify'])) return; //notify enabled? 925 $text = rawLocale('mailtext'); 926 $to = $conf['notify']; 927 $bcc = ''; 928 }elseif($who == 'subscribers'){ 929 if(!$conf['subscribers']) return; //subscribers enabled? 930 if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors 931 $bcc = subscriber_addresslist($id); 932 if(empty($bcc)) return; 933 $to = ''; 934 $text = rawLocale('subscribermail'); 935 }elseif($who == 'register'){ 936 if(empty($conf['registernotify'])) return; 937 $text = rawLocale('registermail'); 938 $to = $conf['registernotify']; 939 $bcc = ''; 940 }else{ 941 return; //just to be safe 942 } 943 944 $ip = clientIP(); 945 $text = str_replace('@DATE@',strftime($conf['dformat']),$text); 946 $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); 947 $text = str_replace('@IPADDRESS@',$ip,$text); 948 $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text); 949 $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text); 950 $text = str_replace('@PAGE@',$id,$text); 951 $text = str_replace('@TITLE@',$conf['title'],$text); 952 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 953 $text = str_replace('@SUMMARY@',$summary,$text); 954 $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); 955 956 foreach ($replace as $key => $substitution) { 957 $text = str_replace('@'.strtoupper($key).'@',$substitution, $text); 958 } 959 960 if($who == 'register'){ 961 $subject = $lang['mail_new_user'].' '.$summary; 962 }elseif($rev){ 963 $subject = $lang['mail_changed'].' '.$id; 964 $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text); 965 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 966 $df = new Diff(split("\n",rawWiki($id,$rev)), 967 split("\n",rawWiki($id))); 968 $dformat = new UnifiedDiffFormatter(); 969 $diff = $dformat->format($df); 970 }else{ 971 $subject=$lang['mail_newpage'].' '.$id; 972 $text = str_replace('@OLDPAGE@','none',$text); 973 $diff = rawWiki($id); 974 } 975 $text = str_replace('@DIFF@',$diff,$text); 976 $subject = '['.$conf['title'].'] '.$subject; 977 978 $from = $conf['mailfrom']; 979 $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from); 980 $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from); 981 $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from); 982 983 mail_send($to,$subject,$text,$from,'',$bcc); 984} 985 986/** 987 * extracts the query from a search engine referrer 988 * 989 * @author Andreas Gohr <andi@splitbrain.org> 990 * @author Todd Augsburger <todd@rollerorgans.com> 991 */ 992function getGoogleQuery(){ 993 $url = parse_url($_SERVER['HTTP_REFERER']); 994 if(!$url) return ''; 995 996 $query = array(); 997 parse_str($url['query'],$query); 998 if(isset($query['q'])) 999 $q = $query['q']; // google, live/msn, aol, ask, altavista, alltheweb, gigablast 1000 elseif(isset($query['p'])) 1001 $q = $query['p']; // yahoo 1002 elseif(isset($query['query'])) 1003 $q = $query['query']; // lycos, netscape, clusty, hotbot 1004 elseif(preg_match("#a9\.com#i",$url['host'])) // a9 1005 $q = urldecode(ltrim($url['path'],'/')); 1006 1007 if(!$q) return ''; 1008 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY); 1009 return $q; 1010} 1011 1012/** 1013 * Try to set correct locale 1014 * 1015 * @deprecated No longer used 1016 * @author Andreas Gohr <andi@splitbrain.org> 1017 */ 1018function setCorrectLocale(){ 1019 global $conf; 1020 global $lang; 1021 1022 $enc = strtoupper($lang['encoding']); 1023 foreach ($lang['locales'] as $loc){ 1024 //try locale 1025 if(@setlocale(LC_ALL,$loc)) return; 1026 //try loceale with encoding 1027 if(@setlocale(LC_ALL,"$loc.$enc")) return; 1028 } 1029 //still here? try to set from environment 1030 @setlocale(LC_ALL,""); 1031} 1032 1033/** 1034 * Return the human readable size of a file 1035 * 1036 * @param int $size A file size 1037 * @param int $dec A number of decimal places 1038 * @author Martin Benjamin <b.martin@cybernet.ch> 1039 * @author Aidan Lister <aidan@php.net> 1040 * @version 1.0.0 1041 */ 1042function filesize_h($size, $dec = 1){ 1043 $sizes = array('B', 'KB', 'MB', 'GB'); 1044 $count = count($sizes); 1045 $i = 0; 1046 1047 while ($size >= 1024 && ($i < $count - 1)) { 1048 $size /= 1024; 1049 $i++; 1050 } 1051 1052 return round($size, $dec) . ' ' . $sizes[$i]; 1053} 1054 1055/** 1056 * return an obfuscated email address in line with $conf['mailguard'] setting 1057 * 1058 * @author Harry Fuecks <hfuecks@gmail.com> 1059 * @author Christopher Smith <chris@jalakai.co.uk> 1060 */ 1061function obfuscate($email) { 1062 global $conf; 1063 1064 switch ($conf['mailguard']) { 1065 case 'visible' : 1066 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1067 return strtr($email, $obfuscate); 1068 1069 case 'hex' : 1070 $encode = ''; 1071 for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';'; 1072 return $encode; 1073 1074 case 'none' : 1075 default : 1076 return $email; 1077 } 1078} 1079 1080/** 1081 * Let us know if a user is tracking a page or a namespace 1082 * 1083 * @author Andreas Gohr <andi@splitbrain.org> 1084 */ 1085function is_subscribed($id,$uid,$ns=false){ 1086 if(!$ns) { 1087 $file=metaFN($id,'.mlist'); 1088 } else { 1089 if(!getNS($id)) { 1090 $file = metaFN(getNS($id),'.mlist'); 1091 } else { 1092 $file = metaFN(getNS($id),'/.mlist'); 1093 } 1094 } 1095 if (@file_exists($file)) { 1096 $mlist = file($file); 1097 $pos = array_search($uid."\n",$mlist); 1098 return is_int($pos); 1099 } 1100 1101 return false; 1102} 1103 1104/** 1105 * Return a string with the email addresses of all the 1106 * users subscribed to a page 1107 * 1108 * @author Steven Danz <steven-danz@kc.rr.com> 1109 */ 1110function subscriber_addresslist($id){ 1111 global $conf; 1112 global $auth; 1113 1114 if (!$conf['subscribers']) return ''; 1115 1116 $users = array(); 1117 $emails = array(); 1118 1119 // load the page mlist file content 1120 $mlist = array(); 1121 $file=metaFN($id,'.mlist'); 1122 if (@file_exists($file)) { 1123 $mlist = file($file); 1124 foreach ($mlist as $who) { 1125 $who = rtrim($who); 1126 $users[$who] = true; 1127 } 1128 } 1129 1130 // load also the namespace mlist file content 1131 $ns = getNS($id); 1132 while ($ns) { 1133 $nsfile = metaFN($ns,'/.mlist'); 1134 if (@file_exists($nsfile)) { 1135 $mlist = file($nsfile); 1136 foreach ($mlist as $who) { 1137 $who = rtrim($who); 1138 $users[$who] = true; 1139 } 1140 } 1141 $ns = getNS($ns); 1142 } 1143 // root namespace 1144 $nsfile = metaFN('','.mlist'); 1145 if (@file_exists($nsfile)) { 1146 $mlist = file($nsfile); 1147 foreach ($mlist as $who) { 1148 $who = rtrim($who); 1149 $users[$who] = true; 1150 } 1151 } 1152 if(!empty($users)) { 1153 foreach (array_keys($users) as $who) { 1154 $info = $auth->getUserData($who); 1155 if($info === false) continue; 1156 $level = auth_aclcheck($id,$who,$info['grps']); 1157 if ($level >= AUTH_READ) { 1158 if (strcasecmp($info['mail'],$conf['notify']) != 0) { 1159 $emails[] = $info['mail']; 1160 } 1161 } 1162 } 1163 } 1164 1165 return implode(',',$emails); 1166} 1167 1168/** 1169 * Removes quoting backslashes 1170 * 1171 * @author Andreas Gohr <andi@splitbrain.org> 1172 */ 1173function unslash($string,$char="'"){ 1174 return str_replace('\\'.$char,$char,$string); 1175} 1176 1177/** 1178 * Convert php.ini shorthands to byte 1179 * 1180 * @author <gilthans dot NO dot SPAM at gmail dot com> 1181 * @link http://de3.php.net/manual/en/ini.core.php#79564 1182 */ 1183function php_to_byte($v){ 1184 $l = substr($v, -1); 1185 $ret = substr($v, 0, -1); 1186 switch(strtoupper($l)){ 1187 case 'P': 1188 $ret *= 1024; 1189 case 'T': 1190 $ret *= 1024; 1191 case 'G': 1192 $ret *= 1024; 1193 case 'M': 1194 $ret *= 1024; 1195 case 'K': 1196 $ret *= 1024; 1197 break; 1198 } 1199 return $ret; 1200} 1201 1202/** 1203 * Wrapper around preg_quote adding the default delimiter 1204 */ 1205function preg_quote_cb($string){ 1206 return preg_quote($string,'/'); 1207} 1208 1209//Setup VIM: ex: et ts=2 enc=utf-8 : 1210