1<?php 2/** 3 * Authentication library 4 * 5 * Including this file will automatically try to login 6 * a user by calling auth_login() 7 * 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @author Andreas Gohr <andi@splitbrain.org> 10 */ 11 12 if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../').'/'); 13 require_once(DOKU_INC.'inc/common.php'); 14 require_once(DOKU_INC.'inc/io.php'); 15 require_once(DOKU_INC.'inc/blowfish.php'); 16 require_once(DOKU_INC.'inc/mail.php'); 17 18 // load the the backend auth functions and instantiate the auth object 19 if (@file_exists(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php')) { 20 require_once(DOKU_INC.'inc/auth/basic.class.php'); 21 require_once(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php'); 22 23 $auth_class = "auth_".$conf['authtype']; 24 if (!class_exists($auth_class)) $auth_class = "auth_basic"; 25 $auth = new $auth_class(); 26 27 // interface between current dokuwiki/old auth system and new style auth object 28 function auth_canDo($fn) { 29 global $auth; 30 return method_exists($auth, $fn); 31 } 32 33 // mandatory functions - these should exist 34 function auth_checkPass($user,$pass) { 35 global $auth; 36 return method_exists($auth,'checkPass') ? $auth->checkPass($user, $pass) : false; 37 } 38 39 function auth_getUserData($user) { 40 global $auth; 41 return method_exists($auth, 'getUserData') ? $auth->getUserData($user) : false; 42 } 43 44 // optional functions, behave gracefully if these don't exist; 45 // potential calling code should query whether these exist in advance 46 function auth_createUser($user,$pass,$name,$mail) { 47 global $auth; 48 return method_exists($auth, 'createUser') ? $auth->createUser($user,$pass,$name,$mail) : null; 49 } 50 51 function auth_modifyUser($user, $changes) { 52 global $auth; 53 return method_exists($auth, 'modifyUser') ? $auth->modifyUser($user,$changes) : false; 54 } 55 56 function auth_deleteUsers($users) { 57 global $auth; 58 return method_exists($auth, 'deleteUsers') ? $auth->deleteUsers($users) : 0; 59 } 60 61 // other functions, will only be accessed by new code 62 //- these must query auth_canDo() or test method existence themselves. 63 64 } else { 65 // old style auth functions 66 require_once(DOKU_INC.'inc/auth/'.$conf['authtype'].'.php'); 67 $auth = null; 68 69 // new function, allows other parts of dokuwiki to know what they can and can't do 70 function auth_canDo($fn) { return function_exists("auth_$fn"); } 71 } 72 73 if (!defined('DOKU_COOKIE')) define('DOKU_COOKIE', 'DW'.md5($conf['title'])); 74 75 // some ACL level defines 76 define('AUTH_NONE',0); 77 define('AUTH_READ',1); 78 define('AUTH_EDIT',2); 79 define('AUTH_CREATE',4); 80 define('AUTH_UPLOAD',8); 81 define('AUTH_DELETE',16); 82 define('AUTH_ADMIN',255); 83 84 if($conf['useacl']){ 85 auth_login($_REQUEST['u'],$_REQUEST['p'],$_REQUEST['r']); 86 //load ACL into a global array 87 if(is_readable(DOKU_CONF.'acl.auth.php')){ 88 $AUTH_ACL = file(DOKU_CONF.'acl.auth.php'); 89 }else{ 90 $AUTH_ACL = array(); 91 } 92 } 93 94/** 95 * This tries to login the user based on the sent auth credentials 96 * 97 * The authentication works like this: if a username was given 98 * a new login is assumed and user/password are checked. If they 99 * are correct the password is encrypted with blowfish and stored 100 * together with the username in a cookie - the same info is stored 101 * in the session, too. Additonally a browserID is stored in the 102 * session. 103 * 104 * If no username was given the cookie is checked: if the username, 105 * crypted password and browserID match between session and cookie 106 * no further testing is done and the user is accepted 107 * 108 * If a cookie was found but no session info was availabe the 109 * blowfish encrypted password from the cookie is decrypted and 110 * together with username rechecked by calling this function again. 111 * 112 * On a successful login $_SERVER[REMOTE_USER] and $USERINFO 113 * are set. 114 * 115 * @author Andreas Gohr <andi@splitbrain.org> 116 * 117 * @param string $user Username 118 * @param string $pass Cleartext Password 119 * @param bool $sticky Cookie should not expire 120 * @return bool true on successful auth 121*/ 122function auth_login($user,$pass,$sticky=false){ 123 global $USERINFO; 124 global $conf; 125 global $lang; 126 $sticky ? $sticky = true : $sticky = false; //sanity check 127 128 if(isset($user)){ 129 //usual login 130 if (auth_checkPass($user,$pass)){ 131 // make logininfo globally available 132 $_SERVER['REMOTE_USER'] = $user; 133 $USERINFO = auth_getUserData($user); //FIXME move all references to session 134 135 // set cookie 136 $pass = PMA_blowfish_encrypt($pass,auth_cookiesalt()); 137 $cookie = base64_encode("$user|$sticky|$pass"); 138 if($sticky) $time = time()+60*60*24*365; //one year 139 setcookie(DOKU_COOKIE,$cookie,$time,'/'); 140 141 // set session 142 $_SESSION[$conf['title']]['auth']['user'] = $user; 143 $_SESSION[$conf['title']]['auth']['pass'] = $pass; 144 $_SESSION[$conf['title']]['auth']['buid'] = auth_browseruid(); 145 $_SESSION[$conf['title']]['auth']['info'] = $USERINFO; 146 return true; 147 }else{ 148 //invalid credentials - log off 149 msg($lang['badlogin'],-1); 150 auth_logoff(); 151 return false; 152 } 153 }else{ 154 // read cookie information 155 $cookie = base64_decode($_COOKIE[DOKU_COOKIE]); 156 list($user,$sticky,$pass) = split('\|',$cookie,3); 157 // get session info 158 $session = $_SESSION[$conf['title']]['auth']; 159 160 if($user && $pass){ 161 // we got a cookie - see if we can trust it 162 if(isset($session) && 163 ($session['user'] == $user) && 164 ($session['pass'] == $pass) && //still crypted 165 ($session['buid'] == auth_browseruid()) ){ 166 // he has session, cookie and browser right - let him in 167 $_SERVER['REMOTE_USER'] = $user; 168 $USERINFO = $session['info']; //FIXME move all references to session 169 return true; 170 } 171 // no we don't trust it yet - recheck pass 172 $pass = PMA_blowfish_decrypt($pass,auth_cookiesalt()); 173 return auth_login($user,$pass,$sticky); 174 } 175 } 176 //just to be sure 177 auth_logoff(); 178 return false; 179} 180 181/** 182 * Builds a pseudo UID from browser and IP data 183 * 184 * This is neither unique nor unfakable - still it adds some 185 * security. Using the first part of the IP makes sure 186 * proxy farms like AOLs are stil okay. 187 * 188 * @author Andreas Gohr <andi@splitbrain.org> 189 * 190 * @return string a MD5 sum of various browser headers 191 */ 192function auth_browseruid(){ 193 $uid = ''; 194 $uid .= $_SERVER['HTTP_USER_AGENT']; 195 $uid .= $_SERVER['HTTP_ACCEPT_ENCODING']; 196 $uid .= $_SERVER['HTTP_ACCEPT_LANGUAGE']; 197 $uid .= $_SERVER['HTTP_ACCEPT_CHARSET']; 198 $uid .= substr($_SERVER['REMOTE_ADDR'],0,strpos($_SERVER['REMOTE_ADDR'],'.')); 199 return md5($uid); 200} 201 202/** 203 * Creates a random key to encrypt the password in cookies 204 * 205 * This function tries to read the password for encrypting 206 * cookies from $conf['metadir'].'/_htcookiesalt' 207 * if no such file is found a random key is created and 208 * and stored in this file. 209 * 210 * @author Andreas Gohr <andi@splitbrain.org> 211 * 212 * @return string 213 */ 214function auth_cookiesalt(){ 215 global $conf; 216 $file = $conf['metadir'].'/_htcookiesalt'; 217 $salt = io_readFile($file); 218 if(empty($salt)){ 219 $salt = uniqid(rand(),true); 220 io_saveFile($file,$salt); 221 } 222 return $salt; 223} 224 225/** 226 * This clears all authenticationdata and thus log the user 227 * off 228 * 229 * @author Andreas Gohr <andi@splitbrain.org> 230 */ 231function auth_logoff(){ 232 global $conf; 233 global $USERINFO; 234 global $INFO, $ID; 235 236 if(isset($_SESSION[$conf['title']]['auth']['user'])) 237 unset($_SESSION[$conf['title']]['auth']['user']); 238 if(isset($_SESSION[$conf['title']]['auth']['pass'])) 239 unset($_SESSION[$conf['title']]['auth']['pass']); 240 if(isset($_SESSION[$conf['title']]['auth']['info'])) 241 unset($_SESSION[$conf['title']]['auth']['info']); 242 if(isset($_SERVER['REMOTE_USER'])) 243 unset($_SERVER['REMOTE_USER']); 244 $USERINFO=null; //FIXME 245 setcookie(DOKU_COOKIE,'',time()-600000,'/'); 246} 247 248/** 249 * Convinience function for auth_aclcheck() 250 * 251 * This checks the permissions for the current user 252 * 253 * @author Andreas Gohr <andi@splitbrain.org> 254 * 255 * @param string $id page ID 256 * @return int permission level 257 */ 258function auth_quickaclcheck($id){ 259 global $conf; 260 global $USERINFO; 261 # if no ACL is used always return upload rights 262 if(!$conf['useacl']) return AUTH_UPLOAD; 263 return auth_aclcheck($id,$_SERVER['REMOTE_USER'],$USERINFO['grps']); 264} 265 266/** 267 * Returns the maximum rights a user has for 268 * the given ID or its namespace 269 * 270 * @author Andreas Gohr <andi@splitbrain.org> 271 * 272 * @param string $id page ID 273 * @param string $user Username 274 * @param array $groups Array of groups the user is in 275 * @return int permission level 276 */ 277function auth_aclcheck($id,$user,$groups){ 278 global $conf; 279 global $AUTH_ACL; 280 281 # if no ACL is used always return upload rights 282 if(!$conf['useacl']) return AUTH_UPLOAD; 283 284 //if user is superuser return 255 (acl_admin) 285 if($conf['superuser'] == $user) { return AUTH_ADMIN; } 286 287 //make sure groups is an array 288 if(!is_array($groups)) $groups = array(); 289 290 //prepend groups with @ 291 $cnt = count($groups); 292 for($i=0; $i<$cnt; $i++){ 293 $groups[$i] = '@'.$groups[$i]; 294 } 295 //if user is in superuser group return 255 (acl_admin) 296 if(in_array($conf['superuser'], $groups)) { return AUTH_ADMIN; } 297 298 $ns = getNS($id); 299 $perm = -1; 300 301 if($user){ 302 //add ALL group 303 $groups[] = '@ALL'; 304 //add User 305 $groups[] = $user; 306 //build regexp 307 $regexp = join('|',$groups); 308 }else{ 309 $regexp = '@ALL'; 310 } 311 312 //check exact match first 313 $matches = preg_grep('/^'.preg_quote($id,'/').'\s+('.$regexp.')\s+/',$AUTH_ACL); 314 if(count($matches)){ 315 foreach($matches as $match){ 316 $match = preg_replace('/#.*$/','',$match); //ignore comments 317 $acl = preg_split('/\s+/',$match); 318 if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 319 if($acl[2] > $perm){ 320 $perm = $acl[2]; 321 } 322 } 323 if($perm > -1){ 324 //we had a match - return it 325 return $perm; 326 } 327 } 328 329 //still here? do the namespace checks 330 if($ns){ 331 $path = $ns.':\*'; 332 }else{ 333 $path = '\*'; //root document 334 } 335 336 do{ 337 $matches = preg_grep('/^'.$path.'\s+('.$regexp.')\s+/',$AUTH_ACL); 338 if(count($matches)){ 339 foreach($matches as $match){ 340 $match = preg_replace('/#.*$/','',$match); //ignore comments 341 $acl = preg_split('/\s+/',$match); 342 if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 343 if($acl[2] > $perm){ 344 $perm = $acl[2]; 345 } 346 } 347 //we had a match - return it 348 return $perm; 349 } 350 351 //get next higher namespace 352 $ns = getNS($ns); 353 354 if($path != '\*'){ 355 $path = $ns.':\*'; 356 if($path == ':\*') $path = '\*'; 357 }else{ 358 //we did this already 359 //looks like there is something wrong with the ACL 360 //break here 361 msg('No ACL setup yet! Denying access to everyone.'); 362 return AUTH_NONE; 363 } 364 }while(1); //this should never loop endless 365 366 //still here? return no permissions 367 return AUTH_NONE; 368} 369 370/** 371 * Create a pronouncable password 372 * 373 * @author Andreas Gohr <andi@splitbrain.org> 374 * @link http://www.phpbuilder.com/annotate/message.php3?id=1014451 375 * 376 * @return string pronouncable password 377 */ 378function auth_pwgen(){ 379 $pw = ''; 380 $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones 381 $v = 'aeiou'; //vowels 382 $a = $c.$v; //both 383 384 //use two syllables... 385 for($i=0;$i < 2; $i++){ 386 $pw .= $c[rand(0, strlen($c)-1)]; 387 $pw .= $v[rand(0, strlen($v)-1)]; 388 $pw .= $a[rand(0, strlen($a)-1)]; 389 } 390 //... and add a nice number 391 $pw .= rand(10,99); 392 393 return $pw; 394} 395 396/** 397 * Sends a password to the given user 398 * 399 * @author Andreas Gohr <andi@splitbrain.org> 400 * 401 * @return bool true on success 402 */ 403function auth_sendPassword($user,$password){ 404 global $conf; 405 global $lang; 406 $hdrs = ''; 407 $userinfo = auth_getUserData($user); 408 409 if(!$userinfo['mail']) return false; 410 411 $text = rawLocale('password'); 412 $text = str_replace('@DOKUWIKIURL@',DOKU_URL,$text); 413 $text = str_replace('@FULLNAME@',$userinfo['name'],$text); 414 $text = str_replace('@LOGIN@',$user,$text); 415 $text = str_replace('@PASSWORD@',$password,$text); 416 $text = str_replace('@TITLE@',$conf['title'],$text); 417 418 return mail_send($userinfo['name'].' <'.$userinfo['mail'].'>', 419 $lang['regpwmail'], 420 $text, 421 $conf['mailfrom']); 422} 423 424/** 425 * Register a new user 426 * 427 * This registers a new user - Data is read directly from $_POST 428 * 429 * @author Andreas Gohr <andi@splitbrain.org> 430 * 431 * @return bool true on success, false on any error 432 */ 433function register(){ 434 global $lang; 435 global $conf; 436 437 if(!$_POST['save']) return false; 438 439 //clean username 440 $_POST['login'] = preg_replace('/.*:/','',$_POST['login']); 441 $_POST['login'] = cleanID($_POST['login']); 442 //clean fullname and email 443 $_POST['fullname'] = trim(str_replace(':','',$_POST['fullname'])); 444 $_POST['email'] = trim(str_replace(':','',$_POST['email'])); 445 446 if( empty($_POST['login']) || 447 empty($_POST['fullname']) || 448 empty($_POST['email']) ){ 449 msg($lang['regmissing'],-1); 450 return false; 451 } 452 453 if ($conf['autopasswd']) { 454 $pass = auth_pwgen(); // automatically generate password 455 } elseif (empty($_POST['pass']) || 456 empty($_POST['passchk'])) { 457 msg($lang['regmissing'], -1); // complain about missing passwords 458 return false; 459 } elseif ($_POST['pass'] != $_POST['passchk']) { 460 msg($lang['regbadpass'], -1); // complain about misspelled passwords 461 return false; 462 } else { 463 $pass = $_POST['pass']; // accept checked and valid password 464 } 465 466 //check mail 467 if(!mail_isvalid($_POST['email'])){ 468 msg($lang['regbadmail'],-1); 469 return false; 470 } 471 472 //okay try to create the user 473 $pass = auth_createUser($_POST['login'],$pass,$_POST['fullname'],$_POST['email']); 474 if(empty($pass)){ 475 msg($lang['reguexists'],-1); 476 return false; 477 } 478 479 if (!$conf['autopasswd']) { 480 msg($lang['regsuccess2'],1); 481 return true; 482 } 483 484 // autogenerated password? then send him the password 485 if (auth_sendPassword($_POST['login'],$pass)){ 486 msg($lang['regsuccess'],1); 487 return true; 488 }else{ 489 msg($lang['regmailfail'],-1); 490 return false; 491 } 492} 493 494/** 495 * Update user profile 496 * 497 * @author Christopher Smith <chris@jalakai.co.uk> 498 */ 499function updateprofile() { 500 global $conf; 501 global $INFO; 502 global $lang; 503 504 if(!$_POST['save']) return false; 505 506 // should not be able to get here without modifyUser being possible... 507 if(!auth_canDo('modifyUser')) { 508 msg($lang['profna'],-1); 509 return false; 510 } 511 512 if ($_POST['newpass'] != $_POST['passchk']) { 513 msg($lang['regbadpass'], -1); // complain about misspelled passwords 514 return false; 515 } 516 517 //clean fullname and email 518 $_POST['fullname'] = trim(str_replace(':','',$_POST['fullname'])); 519 $_POST['email'] = trim(str_replace(':','',$_POST['email'])); 520 521 if (empty($_POST['fullname']) || empty($_POST['email'])) { 522 msg($lang['profnoempty'],-1); 523 return false; 524 } 525 526 if (!mail_isvalid($_POST['email'])){ 527 msg($lang['regbadmail'],-1); 528 return false; 529 } 530 531 if ($_POST['fullname'] != $INFO['userinfo']['name']) $changes['name'] = $_POST['fullname']; 532 if ($_POST['email'] != $INFO['userinfo']['mail']) $changes['mail'] = $_POST['email']; 533 if (!empty($_POST['newpass'])) $changes['pass'] = $_POST['newpass']; 534 535 if (!count($changes)) { 536 msg($lang['profnochange'], -1); 537 return false; 538 } 539 540 if ($conf['profileconfirm']) { 541 if (!auth_verifyPassword($_POST['oldpass'],$INFO['userinfo']['pass'])) { 542 msg($lang['badlogin'],-1); 543 return false; 544 } 545 } 546 547 return auth_modifyUser($_SERVER['REMOTE_USER'], $changes); 548} 549 550/** 551 * Send a new password 552 * 553 * @author Benoit Chesneau <benoit@bchesneau.info> 554 * @author Chris Smith <chris@jalakai.co.uk> 555 * 556 * @return bool true on success, false on any error 557*/ 558function act_resendpwd(){ 559 global $lang; 560 global $conf; 561 562 if(!$_POST['save']) return false; 563 564 // should not be able to get here without modifyUser being possible... 565 if(!auth_canDo('modifyUser')) { 566 msg($lang['resendna'],-1); 567 return false; 568 } 569 570 if (empty($_POST['login'])) { 571 msg($lang['resendpwdmissing'], -1); 572 return false; 573 } else { 574 $user = $_POST['login']; 575 } 576 577 $userinfo = auth_getUserData($user); 578 if(!$userinfo['mail']) { 579 msg($lang['resendpwdnouser'], -1); 580 return false; 581 } 582 583 $pass = auth_pwgen(); 584 if (!auth_modifyUser($user,array('pass' => $pass))) { 585 msg('error modifying user data',-1); 586 return false; 587 } 588 589 if (auth_sendPassword($user,$pass)) { 590 msg($lang['resendpwdsuccess'],1); 591 } else { 592 msg($lang['regmailfail'],-1); 593 } 594 return true; 595} 596 597/** 598 * Uses a regular expresion to check if a given mail address is valid 599 * 600 * May not be completly RFC conform! 601 * 602 * @link http://www.webmasterworld.com/forum88/135.htm 603 * 604 * @param string $email the address to check 605 * @return bool true if address is valid 606 */ 607function isvalidemail($email){ 608 return eregi("^[0-9a-z]([-_.]?[0-9a-z])*@[0-9a-z]([-.]?[0-9a-z])*\\.[a-z]{2,4}$", $email); 609} 610 611/** 612 * Encrypts a password using the given method and salt 613 * 614 * If the selected method needs a salt and none was given, a random one 615 * is chosen. 616 * 617 * The following methods are understood: 618 * 619 * smd5 - Salted MD5 hashing 620 * md5 - Simple MD5 hashing 621 * sha1 - SHA1 hashing 622 * ssha - Salted SHA1 hashing 623 * crypt - Unix crypt 624 * mysql - MySQL password (old method) 625 * my411 - MySQL 4.1.1 password 626 * 627 * @author Andreas Gohr <andi@splitbrain.org> 628 * @return string The crypted password 629 */ 630function auth_cryptPassword($clear,$method='',$salt=''){ 631 global $conf; 632 if(empty($method)) $method = $conf['passcrypt']; 633 634 //prepare a salt 635 if(empty($salt)) $salt = md5(uniqid(rand(), true)); 636 637 switch(strtolower($method)){ 638 case 'smd5': 639 return crypt($clear,'$1$'.substr($salt,0,8).'$'); 640 case 'md5': 641 return md5($clear); 642 case 'sha1': 643 return sha1($clear); 644 case 'ssha': 645 $salt=substr($salt,0,4); 646 return '{SSHA}'.base64_encode(pack("H*", sha1($clear.$salt)).$salt); 647 case 'crypt': 648 return crypt($clear,substr($salt,0,2)); 649 case 'mysql': 650 //from http://www.php.net/mysql comment by <soren at byu dot edu> 651 $nr=0x50305735; 652 $nr2=0x12345671; 653 $add=7; 654 $charArr = preg_split("//", $clear); 655 foreach ($charArr as $char) { 656 if (($char == '') || ($char == ' ') || ($char == '\t')) continue; 657 $charVal = ord($char); 658 $nr ^= ((($nr & 63) + $add) * $charVal) + ($nr << 8); 659 $nr2 += ($nr2 << 8) ^ $nr; 660 $add += $charVal; 661 } 662 return sprintf("%08x%08x", ($nr & 0x7fffffff), ($nr2 & 0x7fffffff)); 663 case 'my411': 664 return '*'.sha1(pack("H*", sha1($clear))); 665 default: 666 msg("Unsupported crypt method $method",-1); 667 } 668} 669 670/** 671 * Verifies a cleartext password against a crypted hash 672 * 673 * The method and salt used for the crypted hash is determined automatically 674 * then the clear text password is crypted using the same method. If both hashs 675 * match true is is returned else false 676 * 677 * @author Andreas Gohr <andi@splitbrain.org> 678 * @return bool 679 */ 680function auth_verifyPassword($clear,$crypt){ 681 $method=''; 682 $salt=''; 683 684 //determine the used method and salt 685 $len = strlen($crypt); 686 if(substr($crypt,0,3) == '$1$'){ 687 $method = 'smd5'; 688 $salt = substr($crypt,3,8); 689 }elseif(substr($crypt,0,6) == '{SSHA}'){ 690 $method = 'ssha'; 691 $salt = substr(base64_decode(substr($crypt, 6)),20); 692 }elseif($len == 32){ 693 $method = 'md5'; 694 }elseif($len == 40){ 695 $method = 'sha1'; 696 }elseif($len == 16){ 697 $method = 'mysql'; 698 }elseif($len == 41 && $crypt[0] == '*'){ 699 $method = 'my411'; 700 }else{ 701 $method = 'crypt'; 702 $salt = substr($crypt,0,2); 703 } 704 705 //crypt and compare 706 if(auth_cryptPassword($clear,$method,$salt) === $crypt){ 707 return true; 708 } 709 return false; 710} 711 712//Setup VIM: ex: et ts=2 enc=utf-8 : 713