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