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