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