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 // mobile detection 212 $info['ismobile'] = clientismobile(); 213 214 return $info; 215} 216 217/** 218 * Build an string of URL parameters 219 * 220 * @author Andreas Gohr 221 */ 222function buildURLparams($params, $sep='&'){ 223 $url = ''; 224 $amp = false; 225 foreach($params as $key => $val){ 226 if($amp) $url .= $sep; 227 228 $url .= $key.'='; 229 $url .= rawurlencode((string)$val); 230 $amp = true; 231 } 232 return $url; 233} 234 235/** 236 * Build an string of html tag attributes 237 * 238 * Skips keys starting with '_', values get HTML encoded 239 * 240 * @author Andreas Gohr 241 */ 242function buildAttributes($params,$skipempty=false){ 243 $url = ''; 244 foreach($params as $key => $val){ 245 if($key{0} == '_') continue; 246 if($val === '' && $skipempty) continue; 247 248 $url .= $key.'="'; 249 $url .= htmlspecialchars ($val); 250 $url .= '" '; 251 } 252 return $url; 253} 254 255 256/** 257 * This builds the breadcrumb trail and returns it as array 258 * 259 * @author Andreas Gohr <andi@splitbrain.org> 260 */ 261function breadcrumbs(){ 262 // we prepare the breadcrumbs early for quick session closing 263 static $crumbs = null; 264 if($crumbs != null) return $crumbs; 265 266 global $ID; 267 global $ACT; 268 global $conf; 269 $crumbs = $_SESSION[DOKU_COOKIE]['bc']; 270 271 //first visit? 272 if (!is_array($crumbs)){ 273 $crumbs = array(); 274 } 275 //we only save on show and existing wiki documents 276 $file = wikiFN($ID); 277 if($ACT != 'show' || !@file_exists($file)){ 278 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 279 return $crumbs; 280 } 281 282 // page names 283 $name = noNSorNS($ID); 284 if ($conf['useheading']) { 285 // get page title 286 $title = p_get_first_heading($ID,true); 287 if ($title) { 288 $name = $title; 289 } 290 } 291 292 //remove ID from array 293 if (isset($crumbs[$ID])) { 294 unset($crumbs[$ID]); 295 } 296 297 //add to array 298 $crumbs[$ID] = $name; 299 //reduce size 300 while(count($crumbs) > $conf['breadcrumbs']){ 301 array_shift($crumbs); 302 } 303 //save to session 304 $_SESSION[DOKU_COOKIE]['bc'] = $crumbs; 305 return $crumbs; 306} 307 308/** 309 * Filter for page IDs 310 * 311 * This is run on a ID before it is outputted somewhere 312 * currently used to replace the colon with something else 313 * on Windows systems and to have proper URL encoding 314 * 315 * Urlencoding is ommitted when the second parameter is false 316 * 317 * @author Andreas Gohr <andi@splitbrain.org> 318 */ 319function idfilter($id,$ue=true){ 320 global $conf; 321 if ($conf['useslash'] && $conf['userewrite']){ 322 $id = strtr($id,':','/'); 323 }elseif (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && 324 $conf['userewrite']) { 325 $id = strtr($id,':',';'); 326 } 327 if($ue){ 328 $id = rawurlencode($id); 329 $id = str_replace('%3A',':',$id); //keep as colon 330 $id = str_replace('%2F','/',$id); //keep as slash 331 } 332 return $id; 333} 334 335/** 336 * This builds a link to a wikipage 337 * 338 * It handles URL rewriting and adds additional parameter if 339 * given in $more 340 * 341 * @author Andreas Gohr <andi@splitbrain.org> 342 */ 343function wl($id='',$more='',$abs=false,$sep='&'){ 344 global $conf; 345 if(is_array($more)){ 346 $more = buildURLparams($more,$sep); 347 }else{ 348 $more = str_replace(',',$sep,$more); 349 } 350 351 $id = idfilter($id); 352 if($abs){ 353 $xlink = DOKU_URL; 354 }else{ 355 $xlink = DOKU_BASE; 356 } 357 358 if($conf['userewrite'] == 2){ 359 $xlink .= DOKU_SCRIPT.'/'.$id; 360 if($more) $xlink .= '?'.$more; 361 }elseif($conf['userewrite']){ 362 $xlink .= $id; 363 if($more) $xlink .= '?'.$more; 364 }elseif($id){ 365 $xlink .= DOKU_SCRIPT.'?id='.$id; 366 if($more) $xlink .= $sep.$more; 367 }else{ 368 $xlink .= DOKU_SCRIPT; 369 if($more) $xlink .= '?'.$more; 370 } 371 372 return $xlink; 373} 374 375/** 376 * This builds a link to an alternate page format 377 * 378 * Handles URL rewriting if enabled. Follows the style of wl(). 379 * 380 * @author Ben Coburn <btcoburn@silicodon.net> 381 */ 382function exportlink($id='',$format='raw',$more='',$abs=false,$sep='&'){ 383 global $conf; 384 if(is_array($more)){ 385 $more = buildURLparams($more,$sep); 386 }else{ 387 $more = str_replace(',',$sep,$more); 388 } 389 390 $format = rawurlencode($format); 391 $id = idfilter($id); 392 if($abs){ 393 $xlink = DOKU_URL; 394 }else{ 395 $xlink = DOKU_BASE; 396 } 397 398 if($conf['userewrite'] == 2){ 399 $xlink .= DOKU_SCRIPT.'/'.$id.'?do=export_'.$format; 400 if($more) $xlink .= $sep.$more; 401 }elseif($conf['userewrite'] == 1){ 402 $xlink .= '_export/'.$format.'/'.$id; 403 if($more) $xlink .= '?'.$more; 404 }else{ 405 $xlink .= DOKU_SCRIPT.'?do=export_'.$format.$sep.'id='.$id; 406 if($more) $xlink .= $sep.$more; 407 } 408 409 return $xlink; 410} 411 412/** 413 * Build a link to a media file 414 * 415 * Will return a link to the detail page if $direct is false 416 * 417 * The $more parameter should always be given as array, the function then 418 * will strip default parameters to produce even cleaner URLs 419 * 420 * @param string $id - the media file id or URL 421 * @param mixed $more - string or array with additional parameters 422 * @param boolean $direct - link to detail page if false 423 * @param string $sep - URL parameter separator 424 * @param boolean $abs - Create an absolute URL 425 */ 426function ml($id='',$more='',$direct=true,$sep='&',$abs=false){ 427 global $conf; 428 if(is_array($more)){ 429 // strip defaults for shorter URLs 430 if(isset($more['cache']) && $more['cache'] == 'cache') unset($more['cache']); 431 if(!$more['w']) unset($more['w']); 432 if(!$more['h']) unset($more['h']); 433 if(isset($more['id']) && $direct) unset($more['id']); 434 $more = buildURLparams($more,$sep); 435 }else{ 436 $more = str_replace('cache=cache','',$more); //skip default 437 $more = str_replace(',,',',',$more); 438 $more = str_replace(',',$sep,$more); 439 } 440 441 if($abs){ 442 $xlink = DOKU_URL; 443 }else{ 444 $xlink = DOKU_BASE; 445 } 446 447 // external URLs are always direct without rewriting 448 if(preg_match('#^(https?|ftp)://#i',$id)){ 449 $xlink .= 'lib/exe/fetch.php'; 450 if($more){ 451 $xlink .= '?'.$more; 452 $xlink .= $sep.'media='.rawurlencode($id); 453 }else{ 454 $xlink .= '?media='.rawurlencode($id); 455 } 456 return $xlink; 457 } 458 459 $id = idfilter($id); 460 461 // decide on scriptname 462 if($direct){ 463 if($conf['userewrite'] == 1){ 464 $script = '_media'; 465 }else{ 466 $script = 'lib/exe/fetch.php'; 467 } 468 }else{ 469 if($conf['userewrite'] == 1){ 470 $script = '_detail'; 471 }else{ 472 $script = 'lib/exe/detail.php'; 473 } 474 } 475 476 // build URL based on rewrite mode 477 if($conf['userewrite']){ 478 $xlink .= $script.'/'.$id; 479 if($more) $xlink .= '?'.$more; 480 }else{ 481 if($more){ 482 $xlink .= $script.'?'.$more; 483 $xlink .= $sep.'media='.$id; 484 }else{ 485 $xlink .= $script.'?media='.$id; 486 } 487 } 488 489 return $xlink; 490} 491 492 493 494/** 495 * Just builds a link to a script 496 * 497 * @todo maybe obsolete 498 * @author Andreas Gohr <andi@splitbrain.org> 499 */ 500function script($script='doku.php'){ 501# $link = getBaseURL(); 502# $link .= $script; 503# return $link; 504 return DOKU_BASE.DOKU_SCRIPT; 505} 506 507/** 508 * Spamcheck against wordlist 509 * 510 * Checks the wikitext against a list of blocked expressions 511 * returns true if the text contains any bad words 512 * 513 * Triggers COMMON_WORDBLOCK_BLOCKED 514 * 515 * Action Plugins can use this event to inspect the blocked data 516 * and gain information about the user who was blocked. 517 * 518 * Event data: 519 * data['matches'] - array of matches 520 * data['userinfo'] - information about the blocked user 521 * [ip] - ip address 522 * [user] - username (if logged in) 523 * [mail] - mail address (if logged in) 524 * [name] - real name (if logged in) 525 * 526 * @author Andreas Gohr <andi@splitbrain.org> 527 * Michael Klier <chi@chimeric.de> 528 */ 529function checkwordblock(){ 530 global $TEXT; 531 global $conf; 532 global $INFO; 533 534 if(!$conf['usewordblock']) return false; 535 536 // we prepare the text a tiny bit to prevent spammers circumventing URL checks 537 $text = preg_replace('!(\b)(www\.[\w.:?\-;,]+?\.[\w.:?\-;,]+?[\w/\#~:.?+=&%@\!\-.:?\-;,]+?)([.:?\-;,]*[^\w/\#~:.?+=&%@\!\-.:?\-;,])!i','\1http://\2 \2\3',$TEXT); 538 539 $wordblocks = getWordblocks(); 540 //how many lines to read at once (to work around some PCRE limits) 541 if(version_compare(phpversion(),'4.3.0','<')){ 542 //old versions of PCRE define a maximum of parenthesises even if no 543 //backreferences are used - the maximum is 99 544 //this is very bad performancewise and may even be too high still 545 $chunksize = 40; 546 }else{ 547 //read file in chunks of 200 - this should work around the 548 //MAX_PATTERN_SIZE in modern PCRE 549 $chunksize = 200; 550 } 551 while($blocks = array_splice($wordblocks,0,$chunksize)){ 552 $re = array(); 553 #build regexp from blocks 554 foreach($blocks as $block){ 555 $block = preg_replace('/#.*$/','',$block); 556 $block = trim($block); 557 if(empty($block)) continue; 558 $re[] = $block; 559 } 560 if(count($re) && preg_match('#('.join('|',$re).')#si',$text,$matches)) { 561 //prepare event data 562 $data['matches'] = $matches; 563 $data['userinfo']['ip'] = $_SERVER['REMOTE_ADDR']; 564 if($_SERVER['REMOTE_USER']) { 565 $data['userinfo']['user'] = $_SERVER['REMOTE_USER']; 566 $data['userinfo']['name'] = $INFO['userinfo']['name']; 567 $data['userinfo']['mail'] = $INFO['userinfo']['mail']; 568 } 569 $callback = create_function('', 'return true;'); 570 return trigger_event('COMMON_WORDBLOCK_BLOCKED', $data, $callback, true); 571 } 572 } 573 return false; 574} 575 576/** 577 * Return the IP of the client 578 * 579 * Honours X-Forwarded-For and X-Real-IP Proxy Headers 580 * 581 * It returns a comma separated list of IPs if the above mentioned 582 * headers are set. If the single parameter is set, it tries to return 583 * a routable public address, prefering the ones suplied in the X 584 * headers 585 * 586 * @param boolean $single If set only a single IP is returned 587 * @author Andreas Gohr <andi@splitbrain.org> 588 */ 589function clientIP($single=false){ 590 $ip = array(); 591 $ip[] = $_SERVER['REMOTE_ADDR']; 592 if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) 593 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_FORWARDED_FOR'])); 594 if(!empty($_SERVER['HTTP_X_REAL_IP'])) 595 $ip = array_merge($ip,explode(',',$_SERVER['HTTP_X_REAL_IP'])); 596 597 // remove any non-IP stuff 598 $cnt = count($ip); 599 $match = array(); 600 for($i=0; $i<$cnt; $i++){ 601 if(preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/',$ip[$i],$match)) { 602 $ip[$i] = $match[0]; 603 } else { 604 $ip[$i] = ''; 605 } 606 if(empty($ip[$i])) unset($ip[$i]); 607 } 608 $ip = array_values(array_unique($ip)); 609 if(!$ip[0]) $ip[0] = '0.0.0.0'; // for some strange reason we don't have a IP 610 611 if(!$single) return join(',',$ip); 612 613 // decide which IP to use, trying to avoid local addresses 614 $ip = array_reverse($ip); 615 foreach($ip as $i){ 616 if(preg_match('/^(127\.|10\.|192\.168\.|172\.((1[6-9])|(2[0-9])|(3[0-1]))\.)/',$i)){ 617 continue; 618 }else{ 619 return $i; 620 } 621 } 622 // still here? just use the first (last) address 623 return $ip[0]; 624} 625 626/** 627 * Check if the browser is on a mobile device 628 * 629 * Adapted from the example code at url below 630 * 631 * @link http://www.brainhandles.com/2007/10/15/detecting-mobile-browsers/#code 632 */ 633function clientismobile(){ 634 635 if(isset($_SERVER['HTTP_X_WAP_PROFILE'])) return true; 636 637 if(preg_match('/wap\.|\.wap/i',$_SERVER['HTTP_ACCEPT'])) return true; 638 639 if(!isset($_SERVER['HTTP_USER_AGENT'])) return false; 640 641 $uamatches = 'midp|j2me|avantg|docomo|novarra|palmos|palmsource|240x320|opwv|chtml|pda|windows ce|mmp\/|blackberry|mib\/|symbian|wireless|nokia|hand|mobi|phone|cdm|up\.b|audio|SIE\-|SEC\-|samsung|HTC|mot\-|mitsu|sagem|sony|alcatel|lg|erics|vx|NEC|philips|mmm|xx|panasonic|sharp|wap|sch|rover|pocket|benq|java|pt|pg|vox|amoi|bird|compal|kg|voda|sany|kdd|dbt|sendo|sgh|gradi|jb|\d\d\di|moto'; 642 643 if(preg_match("/$uamatches/i",$_SERVER['HTTP_USER_AGENT'])) return true; 644 645 return false; 646} 647 648 649/** 650 * Convert one or more comma separated IPs to hostnames 651 * 652 * @author Glen Harris <astfgl@iamnota.org> 653 * @returns a comma separated list of hostnames 654 */ 655function gethostsbyaddrs($ips){ 656 $hosts = array(); 657 $ips = explode(',',$ips); 658 659 if(is_array($ips)) { 660 foreach($ips as $ip){ 661 $hosts[] = gethostbyaddr(trim($ip)); 662 } 663 return join(',',$hosts); 664 } else { 665 return gethostbyaddr(trim($ips)); 666 } 667} 668 669/** 670 * Checks if a given page is currently locked. 671 * 672 * removes stale lockfiles 673 * 674 * @author Andreas Gohr <andi@splitbrain.org> 675 */ 676function checklock($id){ 677 global $conf; 678 $lock = wikiLockFN($id); 679 680 //no lockfile 681 if(!@file_exists($lock)) return false; 682 683 //lockfile expired 684 if((time() - filemtime($lock)) > $conf['locktime']){ 685 @unlink($lock); 686 return false; 687 } 688 689 //my own lock 690 $ip = io_readFile($lock); 691 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 692 return false; 693 } 694 695 return $ip; 696} 697 698/** 699 * Lock a page for editing 700 * 701 * @author Andreas Gohr <andi@splitbrain.org> 702 */ 703function lock($id){ 704 $lock = wikiLockFN($id); 705 if($_SERVER['REMOTE_USER']){ 706 io_saveFile($lock,$_SERVER['REMOTE_USER']); 707 }else{ 708 io_saveFile($lock,clientIP()); 709 } 710} 711 712/** 713 * Unlock a page if it was locked by the user 714 * 715 * @author Andreas Gohr <andi@splitbrain.org> 716 * @return bool true if a lock was removed 717 */ 718function unlock($id){ 719 $lock = wikiLockFN($id); 720 if(@file_exists($lock)){ 721 $ip = io_readFile($lock); 722 if( ($ip == clientIP()) || ($ip == $_SERVER['REMOTE_USER']) ){ 723 @unlink($lock); 724 return true; 725 } 726 } 727 return false; 728} 729 730/** 731 * convert line ending to unix format 732 * 733 * @see formText() for 2crlf conversion 734 * @author Andreas Gohr <andi@splitbrain.org> 735 */ 736function cleanText($text){ 737 $text = preg_replace("/(\015\012)|(\015)/","\012",$text); 738 return $text; 739} 740 741/** 742 * Prepares text for print in Webforms by encoding special chars. 743 * It also converts line endings to Windows format which is 744 * pseudo standard for webforms. 745 * 746 * @see cleanText() for 2unix conversion 747 * @author Andreas Gohr <andi@splitbrain.org> 748 */ 749function formText($text){ 750 $text = str_replace("\012","\015\012",$text); 751 return htmlspecialchars($text); 752} 753 754/** 755 * Returns the specified local text in raw format 756 * 757 * @author Andreas Gohr <andi@splitbrain.org> 758 */ 759function rawLocale($id){ 760 return io_readFile(localeFN($id)); 761} 762 763/** 764 * Returns the raw WikiText 765 * 766 * @author Andreas Gohr <andi@splitbrain.org> 767 */ 768function rawWiki($id,$rev=''){ 769 return io_readWikiPage(wikiFN($id, $rev), $id, $rev); 770} 771 772/** 773 * Returns the pagetemplate contents for the ID's namespace 774 * 775 * @author Andreas Gohr <andi@splitbrain.org> 776 */ 777function pageTemplate($data){ 778 $id = $data[0]; 779 global $conf; 780 global $INFO; 781 782 $path = dirname(wikiFN($id)); 783 784 if(@file_exists($path.'/_template.txt')){ 785 $tpl = io_readFile($path.'/_template.txt'); 786 }else{ 787 // search upper namespaces for templates 788 $len = strlen(rtrim($conf['datadir'],'/')); 789 while (strlen($path) >= $len){ 790 if(@file_exists($path.'/__template.txt')){ 791 $tpl = io_readFile($path.'/__template.txt'); 792 break; 793 } 794 $path = substr($path, 0, strrpos($path, '/')); 795 } 796 } 797 if(!$tpl) return ''; 798 799 // replace placeholders 800 $tpl = str_replace('@ID@',$id,$tpl); 801 $tpl = str_replace('@NS@',getNS($id),$tpl); 802 $tpl = str_replace('@PAGE@',strtr(noNS($id),'_',' '),$tpl); 803 $tpl = str_replace('@USER@',$_SERVER['REMOTE_USER'],$tpl); 804 $tpl = str_replace('@NAME@',$INFO['userinfo']['name'],$tpl); 805 $tpl = str_replace('@MAIL@',$INFO['userinfo']['mail'],$tpl); 806 $tpl = str_replace('@DATE@',$conf['dformat'],$tpl); 807 // we need the callback to work around strftime's char limit 808 $tpl = preg_replace_callback('/%./',create_function('$m','return strftime($m[0]);'),$tpl); 809 810 return $tpl; 811} 812 813 814/** 815 * Returns the raw Wiki Text in three slices. 816 * 817 * The range parameter needs to have the form "from-to" 818 * and gives the range of the section in bytes - no 819 * UTF-8 awareness is needed. 820 * The returned order is prefix, section and suffix. 821 * 822 * @author Andreas Gohr <andi@splitbrain.org> 823 */ 824function rawWikiSlices($range,$id,$rev=''){ 825 list($from,$to) = split('-',$range,2); 826 $text = io_readWikiPage(wikiFN($id, $rev), $id, $rev); 827 if(!$from) $from = 0; 828 if(!$to) $to = strlen($text)+1; 829 830 $slices[0] = substr($text,0,$from-1); 831 $slices[1] = substr($text,$from-1,$to-$from); 832 $slices[2] = substr($text,$to); 833 834 return $slices; 835} 836 837/** 838 * Joins wiki text slices 839 * 840 * function to join the text slices with correct lineendings again. 841 * When the pretty parameter is set to true it adds additional empty 842 * lines between sections if needed (used on saving). 843 * 844 * @author Andreas Gohr <andi@splitbrain.org> 845 */ 846function con($pre,$text,$suf,$pretty=false){ 847 848 if($pretty){ 849 if($pre && substr($pre,-1) != "\n") $pre .= "\n"; 850 if($suf && substr($text,-1) != "\n") $text .= "\n"; 851 } 852 853 // Avoid double newline above section when saving section edit 854 //if($pre) $pre .= "\n"; 855 if($suf) $text .= "\n"; 856 return $pre.$text.$suf; 857} 858 859/** 860 * Saves a wikitext by calling io_writeWikiPage. 861 * Also directs changelog and attic updates. 862 * 863 * @author Andreas Gohr <andi@splitbrain.org> 864 * @author Ben Coburn <btcoburn@silicodon.net> 865 */ 866function saveWikiText($id,$text,$summary,$minor=false){ 867 /* Note to developers: 868 This code is subtle and delicate. Test the behavior of 869 the attic and changelog with dokuwiki and external edits 870 after any changes. External edits change the wiki page 871 directly without using php or dokuwiki. 872 */ 873 global $conf; 874 global $lang; 875 global $REV; 876 // ignore if no changes were made 877 if($text == rawWiki($id,'')){ 878 return; 879 } 880 881 $file = wikiFN($id); 882 $old = @filemtime($file); // from page 883 $wasRemoved = empty($text); 884 $wasCreated = !@file_exists($file); 885 $wasReverted = ($REV==true); 886 $newRev = false; 887 $oldRev = getRevisions($id, -1, 1, 1024); // from changelog 888 $oldRev = (int)(empty($oldRev)?0:$oldRev[0]); 889 if(!@file_exists(wikiFN($id, $old)) && @file_exists($file) && $old>=$oldRev) { 890 // add old revision to the attic if missing 891 saveOldRevision($id); 892 // add a changelog entry if this edit came from outside dokuwiki 893 if ($old>$oldRev) { 894 addLogEntry($old, $id, DOKU_CHANGE_TYPE_EDIT, $lang['external_edit'], '', array('ExternalEdit'=>true)); 895 // remove soon to be stale instructions 896 $cache = new cache_instructions($id, $file); 897 $cache->removeCache(); 898 } 899 } 900 901 if ($wasRemoved){ 902 // Send "update" event with empty data, so plugins can react to page deletion 903 $data = array(array($file, '', false), getNS($id), noNS($id), false); 904 trigger_event('IO_WIKIPAGE_WRITE', $data); 905 // pre-save deleted revision 906 @touch($file); 907 clearstatcache(); 908 $newRev = saveOldRevision($id); 909 // remove empty file 910 @unlink($file); 911 // remove old meta info... 912 $mfiles = metaFiles($id); 913 $changelog = metaFN($id, '.changes'); 914 $metadata = metaFN($id, '.meta'); 915 foreach ($mfiles as $mfile) { 916 // but keep per-page changelog to preserve page history and keep meta data 917 if (@file_exists($mfile) && $mfile!==$changelog && $mfile!==$metadata) { @unlink($mfile); } 918 } 919 // purge meta data 920 p_purge_metadata($id); 921 $del = true; 922 // autoset summary on deletion 923 if(empty($summary)) $summary = $lang['deleted']; 924 // remove empty namespaces 925 io_sweepNS($id, 'datadir'); 926 io_sweepNS($id, 'mediadir'); 927 }else{ 928 // save file (namespace dir is created in io_writeWikiPage) 929 io_writeWikiPage($file, $text, $id); 930 // pre-save the revision, to keep the attic in sync 931 $newRev = saveOldRevision($id); 932 $del = false; 933 } 934 935 // select changelog line type 936 $extra = ''; 937 $type = DOKU_CHANGE_TYPE_EDIT; 938 if ($wasReverted) { 939 $type = DOKU_CHANGE_TYPE_REVERT; 940 $extra = $REV; 941 } 942 else if ($wasCreated) { $type = DOKU_CHANGE_TYPE_CREATE; } 943 else if ($wasRemoved) { $type = DOKU_CHANGE_TYPE_DELETE; } 944 else if ($minor && $conf['useacl'] && $_SERVER['REMOTE_USER']) { $type = DOKU_CHANGE_TYPE_MINOR_EDIT; } //minor edits only for logged in users 945 946 addLogEntry($newRev, $id, $type, $summary, $extra); 947 // send notify mails 948 notify($id,'admin',$old,$summary,$minor); 949 notify($id,'subscribers',$old,$summary,$minor); 950 951 // update the purgefile (timestamp of the last time anything within the wiki was changed) 952 io_saveFile($conf['cachedir'].'/purgefile',time()); 953 954 // if useheading is enabled, purge the cache of all linking pages 955 if($conf['useheading']){ 956 require_once(DOKU_INC.'inc/fulltext.php'); 957 $pages = ft_backlinks($id); 958 foreach ($pages as $page) { 959 $cache = new cache_renderer($page, wikiFN($page), 'xhtml'); 960 $cache->removeCache(); 961 } 962 } 963} 964 965/** 966 * moves the current version to the attic and returns its 967 * revision date 968 * 969 * @author Andreas Gohr <andi@splitbrain.org> 970 */ 971function saveOldRevision($id){ 972 global $conf; 973 $oldf = wikiFN($id); 974 if(!@file_exists($oldf)) return ''; 975 $date = filemtime($oldf); 976 $newf = wikiFN($id,$date); 977 io_writeWikiPage($newf, rawWiki($id), $id, $date); 978 return $date; 979} 980 981/** 982 * Sends a notify mail on page change 983 * 984 * @param string $id The changed page 985 * @param string $who Who to notify (admin|subscribers) 986 * @param int $rev Old page revision 987 * @param string $summary What changed 988 * @param boolean $minor Is this a minor edit? 989 * @param array $replace Additional string substitutions, @KEY@ to be replaced by value 990 * 991 * @author Andreas Gohr <andi@splitbrain.org> 992 */ 993function notify($id,$who,$rev='',$summary='',$minor=false,$replace=array()){ 994 global $lang; 995 global $conf; 996 global $INFO; 997 998 // decide if there is something to do 999 if($who == 'admin'){ 1000 if(empty($conf['notify'])) return; //notify enabled? 1001 $text = rawLocale('mailtext'); 1002 $to = $conf['notify']; 1003 $bcc = ''; 1004 }elseif($who == 'subscribers'){ 1005 if(!$conf['subscribers']) return; //subscribers enabled? 1006 if($conf['useacl'] && $_SERVER['REMOTE_USER'] && $minor) return; //skip minors 1007 $bcc = subscriber_addresslist($id); 1008 if(empty($bcc)) return; 1009 $to = ''; 1010 $text = rawLocale('subscribermail'); 1011 }elseif($who == 'register'){ 1012 if(empty($conf['registernotify'])) return; 1013 $text = rawLocale('registermail'); 1014 $to = $conf['registernotify']; 1015 $bcc = ''; 1016 }else{ 1017 return; //just to be safe 1018 } 1019 1020 $ip = clientIP(); 1021 $text = str_replace('@DATE@',strftime($conf['dformat']),$text); 1022 $text = str_replace('@BROWSER@',$_SERVER['HTTP_USER_AGENT'],$text); 1023 $text = str_replace('@IPADDRESS@',$ip,$text); 1024 $text = str_replace('@HOSTNAME@',gethostsbyaddrs($ip),$text); 1025 $text = str_replace('@NEWPAGE@',wl($id,'',true,'&'),$text); 1026 $text = str_replace('@PAGE@',$id,$text); 1027 $text = str_replace('@TITLE@',$conf['title'],$text); 1028 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 1029 $text = str_replace('@SUMMARY@',$summary,$text); 1030 $text = str_replace('@USER@',$_SERVER['REMOTE_USER'],$text); 1031 1032 foreach ($replace as $key => $substitution) { 1033 $text = str_replace('@'.strtoupper($key).'@',$substitution, $text); 1034 } 1035 1036 if($who == 'register'){ 1037 $subject = $lang['mail_new_user'].' '.$summary; 1038 }elseif($rev){ 1039 $subject = $lang['mail_changed'].' '.$id; 1040 $text = str_replace('@OLDPAGE@',wl($id,"rev=$rev",true,'&'),$text); 1041 require_once(DOKU_INC.'inc/DifferenceEngine.php'); 1042 $df = new Diff(split("\n",rawWiki($id,$rev)), 1043 split("\n",rawWiki($id))); 1044 $dformat = new UnifiedDiffFormatter(); 1045 $diff = $dformat->format($df); 1046 }else{ 1047 $subject=$lang['mail_newpage'].' '.$id; 1048 $text = str_replace('@OLDPAGE@','none',$text); 1049 $diff = rawWiki($id); 1050 } 1051 $text = str_replace('@DIFF@',$diff,$text); 1052 $subject = '['.$conf['title'].'] '.$subject; 1053 1054 $from = $conf['mailfrom']; 1055 $from = str_replace('@USER@',$_SERVER['REMOTE_USER'],$from); 1056 $from = str_replace('@NAME@',$INFO['userinfo']['name'],$from); 1057 $from = str_replace('@MAIL@',$INFO['userinfo']['mail'],$from); 1058 1059 mail_send($to,$subject,$text,$from,'',$bcc); 1060} 1061 1062/** 1063 * extracts the query from a search engine referrer 1064 * 1065 * @author Andreas Gohr <andi@splitbrain.org> 1066 * @author Todd Augsburger <todd@rollerorgans.com> 1067 */ 1068function getGoogleQuery(){ 1069 $url = parse_url($_SERVER['HTTP_REFERER']); 1070 if(!$url) return ''; 1071 1072 $query = array(); 1073 parse_str($url['query'],$query); 1074 if(isset($query['q'])) 1075 $q = $query['q']; // google, live/msn, aol, ask, altavista, alltheweb, gigablast 1076 elseif(isset($query['p'])) 1077 $q = $query['p']; // yahoo 1078 elseif(isset($query['query'])) 1079 $q = $query['query']; // lycos, netscape, clusty, hotbot 1080 elseif(preg_match("#a9\.com#i",$url['host'])) // a9 1081 $q = urldecode(ltrim($url['path'],'/')); 1082 1083 if(!$q) return ''; 1084 $q = preg_split('/[\s\'"\\\\`()\]\[?:!\.{};,#+*<>\\/]+/',$q,-1,PREG_SPLIT_NO_EMPTY); 1085 return $q; 1086} 1087 1088/** 1089 * Try to set correct locale 1090 * 1091 * @deprecated No longer used 1092 * @author Andreas Gohr <andi@splitbrain.org> 1093 */ 1094function setCorrectLocale(){ 1095 global $conf; 1096 global $lang; 1097 1098 $enc = strtoupper($lang['encoding']); 1099 foreach ($lang['locales'] as $loc){ 1100 //try locale 1101 if(@setlocale(LC_ALL,$loc)) return; 1102 //try loceale with encoding 1103 if(@setlocale(LC_ALL,"$loc.$enc")) return; 1104 } 1105 //still here? try to set from environment 1106 @setlocale(LC_ALL,""); 1107} 1108 1109/** 1110 * Return the human readable size of a file 1111 * 1112 * @param int $size A file size 1113 * @param int $dec A number of decimal places 1114 * @author Martin Benjamin <b.martin@cybernet.ch> 1115 * @author Aidan Lister <aidan@php.net> 1116 * @version 1.0.0 1117 */ 1118function filesize_h($size, $dec = 1){ 1119 $sizes = array('B', 'KB', 'MB', 'GB'); 1120 $count = count($sizes); 1121 $i = 0; 1122 1123 while ($size >= 1024 && ($i < $count - 1)) { 1124 $size /= 1024; 1125 $i++; 1126 } 1127 1128 return round($size, $dec) . ' ' . $sizes[$i]; 1129} 1130 1131/** 1132 * return an obfuscated email address in line with $conf['mailguard'] setting 1133 * 1134 * @author Harry Fuecks <hfuecks@gmail.com> 1135 * @author Christopher Smith <chris@jalakai.co.uk> 1136 */ 1137function obfuscate($email) { 1138 global $conf; 1139 1140 switch ($conf['mailguard']) { 1141 case 'visible' : 1142 $obfuscate = array('@' => ' [at] ', '.' => ' [dot] ', '-' => ' [dash] '); 1143 return strtr($email, $obfuscate); 1144 1145 case 'hex' : 1146 $encode = ''; 1147 for ($x=0; $x < strlen($email); $x++) $encode .= '&#x' . bin2hex($email{$x}).';'; 1148 return $encode; 1149 1150 case 'none' : 1151 default : 1152 return $email; 1153 } 1154} 1155 1156/** 1157 * Let us know if a user is tracking a page or a namespace 1158 * 1159 * @author Andreas Gohr <andi@splitbrain.org> 1160 */ 1161function is_subscribed($id,$uid,$ns=false){ 1162 if(!$ns) { 1163 $file=metaFN($id,'.mlist'); 1164 } else { 1165 if(!getNS($id)) { 1166 $file = metaFN(getNS($id),'.mlist'); 1167 } else { 1168 $file = metaFN(getNS($id),'/.mlist'); 1169 } 1170 } 1171 if (@file_exists($file)) { 1172 $mlist = file($file); 1173 $pos = array_search($uid."\n",$mlist); 1174 return is_int($pos); 1175 } 1176 1177 return false; 1178} 1179 1180/** 1181 * Return a string with the email addresses of all the 1182 * users subscribed to a page 1183 * 1184 * @author Steven Danz <steven-danz@kc.rr.com> 1185 */ 1186function subscriber_addresslist($id){ 1187 global $conf; 1188 global $auth; 1189 1190 if (!$conf['subscribers']) return ''; 1191 1192 $users = array(); 1193 $emails = array(); 1194 1195 // load the page mlist file content 1196 $mlist = array(); 1197 $file=metaFN($id,'.mlist'); 1198 if (@file_exists($file)) { 1199 $mlist = file($file); 1200 foreach ($mlist as $who) { 1201 $who = rtrim($who); 1202 $users[$who] = true; 1203 } 1204 } 1205 1206 // load also the namespace mlist file content 1207 $ns = getNS($id); 1208 while ($ns) { 1209 $nsfile = metaFN($ns,'/.mlist'); 1210 if (@file_exists($nsfile)) { 1211 $mlist = file($nsfile); 1212 foreach ($mlist as $who) { 1213 $who = rtrim($who); 1214 $users[$who] = true; 1215 } 1216 } 1217 $ns = getNS($ns); 1218 } 1219 // root namespace 1220 $nsfile = metaFN('','.mlist'); 1221 if (@file_exists($nsfile)) { 1222 $mlist = file($nsfile); 1223 foreach ($mlist as $who) { 1224 $who = rtrim($who); 1225 $users[$who] = true; 1226 } 1227 } 1228 if(!empty($users)) { 1229 foreach (array_keys($users) as $who) { 1230 $info = $auth->getUserData($who); 1231 if($info === false) continue; 1232 $level = auth_aclcheck($id,$who,$info['grps']); 1233 if ($level >= AUTH_READ) { 1234 if (strcasecmp($info['mail'],$conf['notify']) != 0) { 1235 $emails[] = $info['mail']; 1236 } 1237 } 1238 } 1239 } 1240 1241 return implode(',',$emails); 1242} 1243 1244/** 1245 * Removes quoting backslashes 1246 * 1247 * @author Andreas Gohr <andi@splitbrain.org> 1248 */ 1249function unslash($string,$char="'"){ 1250 return str_replace('\\'.$char,$char,$string); 1251} 1252 1253/** 1254 * Convert php.ini shorthands to byte 1255 * 1256 * @author <gilthans dot NO dot SPAM at gmail dot com> 1257 * @link http://de3.php.net/manual/en/ini.core.php#79564 1258 */ 1259function php_to_byte($v){ 1260 $l = substr($v, -1); 1261 $ret = substr($v, 0, -1); 1262 switch(strtoupper($l)){ 1263 case 'P': 1264 $ret *= 1024; 1265 case 'T': 1266 $ret *= 1024; 1267 case 'G': 1268 $ret *= 1024; 1269 case 'M': 1270 $ret *= 1024; 1271 case 'K': 1272 $ret *= 1024; 1273 break; 1274 } 1275 return $ret; 1276} 1277 1278/** 1279 * Wrapper around preg_quote adding the default delimiter 1280 */ 1281function preg_quote_cb($string){ 1282 return preg_quote($string,'/'); 1283} 1284 1285/** 1286 * Shorten a given string by removing data from the middle 1287 * 1288 * You can give the string in two parts, teh first part $keep 1289 * will never be shortened. The second part $short will be cut 1290 * in the middle to shorten but only if at least $min chars are 1291 * left to display it. Otherwise it will be left off. 1292 * 1293 * @param string $keep the part to keep 1294 * @param string $short the part to shorten 1295 * @param int $max maximum chars you want for the whole string 1296 * @param int $min minimum number of chars to have left for middle shortening 1297 * @param string $char the shortening character to use 1298 */ 1299function shorten($keep,$short,$max,$min=9,$char='⌇'){ 1300 $max = $max - utf8_strlen($keep); 1301 if($max < $min) return $keep; 1302 $len = utf8_strlen($short); 1303 if($len <= $max) return $keep.$short; 1304 $half = floor($max/2); 1305 return $keep.utf8_substr($short,0,$half-1).$char.utf8_substr($short,$len-$half); 1306} 1307 1308/** 1309 * Return the users realname or e-mail address for use 1310 * in page footer and recent changes pages 1311 * 1312 * @author Andy Webber <dokuwiki AT andywebber DOT com> 1313 */ 1314function editorinfo($username){ 1315 global $conf; 1316 global $auth; 1317 1318 switch($conf['showuseras']){ 1319 case 'username': 1320 case 'email': 1321 case 'email_link': 1322 $info = $auth->getUserData($username); 1323 break; 1324 default: 1325 return hsc($username); 1326 } 1327 1328 if(isset($info) && $info) { 1329 switch($conf['showuseras']){ 1330 case 'username': 1331 return hsc($info['name']); 1332 case 'email': 1333 return obfuscate($info['mail']); 1334 case 'email_link': 1335 $mail=obfuscate($info['mail']); 1336 return '<a href="mailto:'.$mail.'">'.$mail.'</a>'; 1337 default: 1338 return hsc($username); 1339 } 1340 } else { 1341 return hsc($username); 1342 } 1343} 1344 1345/** 1346 * Returns the path to a image file for the currently chosen license. 1347 * When no image exists, returns an empty string 1348 * 1349 * @author Andreas Gohr <andi@splitbrain.org> 1350 * @param string $type - type of image 'badge' or 'button' 1351 */ 1352function license_img($type){ 1353 global $license; 1354 global $conf; 1355 if(!$conf['license']) return ''; 1356 if(!is_array($license[$conf['license']])) return ''; 1357 $lic = $license[$conf['license']]; 1358 $try = array(); 1359 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.png'; 1360 $try[] = 'lib/images/license/'.$type.'/'.$conf['license'].'.gif'; 1361 if(substr($conf['license'],0,3) == 'cc-'){ 1362 $try[] = 'lib/images/license/'.$type.'/cc.png'; 1363 } 1364 foreach($try as $src){ 1365 if(@file_exists(DOKU_INC.$src)) return $src; 1366 } 1367 return ''; 1368} 1369 1370//Setup VIM: ex: et ts=2 enc=utf-8 : 1371