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