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