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