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