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