1<?php 2 3/** 4 * Authentication library 5 * 6 * Including this file will automatically try to login 7 * a user by calling auth_login() 8 * 9 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 10 * @author Andreas Gohr <andi@splitbrain.org> 11 */ 12 13use phpseclib\Crypt\AES; 14use dokuwiki\Utf8\PhpString; 15use dokuwiki\Extension\AuthPlugin; 16use dokuwiki\Extension\Event; 17use dokuwiki\Extension\PluginController; 18use dokuwiki\PassHash; 19use dokuwiki\Subscriptions\RegistrationSubscriptionSender; 20 21/** 22 * Initialize the auth system. 23 * 24 * This function is automatically called at the end of init.php 25 * 26 * This used to be the main() of the auth.php 27 * 28 * @todo backend loading maybe should be handled by the class autoloader 29 * @todo maybe split into multiple functions at the XXX marked positions 30 * @triggers AUTH_LOGIN_CHECK 31 * @return bool 32 */ 33function auth_setup() 34{ 35 global $conf; 36 /* @var AuthPlugin $auth */ 37 global $auth; 38 /* @var Input $INPUT */ 39 global $INPUT; 40 global $AUTH_ACL; 41 global $lang; 42 /* @var PluginController $plugin_controller */ 43 global $plugin_controller; 44 $AUTH_ACL = []; 45 46 if (!$conf['useacl']) return false; 47 48 // try to load auth backend from plugins 49 foreach ($plugin_controller->getList('auth') as $plugin) { 50 if ($conf['authtype'] === $plugin) { 51 $auth = $plugin_controller->load('auth', $plugin); 52 break; 53 } 54 } 55 56 if (!$auth instanceof AuthPlugin) { 57 msg($lang['authtempfail'], -1); 58 return false; 59 } 60 61 if ($auth->success == false) { 62 // degrade to unauthenticated user 63 $auth = null; 64 auth_logoff(); 65 msg($lang['authtempfail'], -1); 66 return false; 67 } 68 69 // do the login either by cookie or provided credentials XXX 70 $INPUT->set('http_credentials', false); 71 if (!$conf['rememberme']) $INPUT->set('r', false); 72 73 // Populate Basic Auth user/password from Authorization header 74 // Note: with FastCGI, data is in REDIRECT_HTTP_AUTHORIZATION instead of HTTP_AUTHORIZATION 75 $header = $INPUT->server->str('HTTP_AUTHORIZATION') ?: $INPUT->server->str('REDIRECT_HTTP_AUTHORIZATION'); 76 if (preg_match('~^Basic ([a-z\d/+]*={0,2})$~i', $header, $matches)) { 77 $userpass = explode(':', base64_decode($matches[1])); 78 [$_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']] = $userpass; 79 } 80 81 // if no credentials were given try to use HTTP auth (for SSO) 82 if (!$INPUT->str('u') && empty($_COOKIE[DOKU_COOKIE]) && !empty($INPUT->server->str('PHP_AUTH_USER'))) { 83 $INPUT->set('u', $INPUT->server->str('PHP_AUTH_USER')); 84 $INPUT->set('p', $INPUT->server->str('PHP_AUTH_PW')); 85 $INPUT->set('http_credentials', true); 86 } 87 88 // apply cleaning (auth specific user names, remove control chars) 89 if (true === $auth->success) { 90 $INPUT->set('u', $auth->cleanUser(stripctl($INPUT->str('u')))); 91 $INPUT->set('p', stripctl($INPUT->str('p'))); 92 } 93 94 $ok = null; 95 if ($auth instanceof AuthPlugin && $auth->canDo('external')) { 96 $ok = $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r')); 97 } 98 99 if ($ok === null) { 100 // external trust mechanism not in place, or returns no result, 101 // then attempt auth_login 102 $evdata = [ 103 'user' => $INPUT->str('u'), 104 'password' => $INPUT->str('p'), 105 'sticky' => $INPUT->bool('r'), 106 'silent' => $INPUT->bool('http_credentials') 107 ]; 108 Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper'); 109 } 110 111 //load ACL into a global array XXX 112 $AUTH_ACL = auth_loadACL(); 113 114 return true; 115} 116 117/** 118 * Loads the ACL setup and handle user wildcards 119 * 120 * @author Andreas Gohr <andi@splitbrain.org> 121 * 122 * @return array 123 */ 124function auth_loadACL() 125{ 126 global $config_cascade; 127 global $USERINFO; 128 /* @var Input $INPUT */ 129 global $INPUT; 130 131 if (!is_readable($config_cascade['acl']['default'])) return []; 132 133 $acl = file($config_cascade['acl']['default']); 134 135 $out = []; 136 foreach ($acl as $line) { 137 $line = trim($line); 138 if (empty($line) || ($line[0] == '#')) continue; // skip blank lines & comments 139 [$id, $rest] = preg_split('/[ \t]+/', $line, 2); 140 141 // substitute user wildcard first (its 1:1) 142 if (strstr($line, '%USER%')) { 143 // if user is not logged in, this ACL line is meaningless - skip it 144 if (!$INPUT->server->has('REMOTE_USER')) continue; 145 146 $id = str_replace('%USER%', cleanID($INPUT->server->str('REMOTE_USER')), $id); 147 $rest = str_replace('%USER%', auth_nameencode($INPUT->server->str('REMOTE_USER')), $rest); 148 } 149 150 // substitute group wildcard (its 1:m) 151 if (strstr($line, '%GROUP%')) { 152 // if user is not logged in, grps is empty, no output will be added (i.e. skipped) 153 if (isset($USERINFO['grps'])) { 154 foreach ((array) $USERINFO['grps'] as $grp) { 155 $nid = str_replace('%GROUP%', cleanID($grp), $id); 156 $nrest = str_replace('%GROUP%', '@' . auth_nameencode($grp), $rest); 157 $out[] = "$nid\t$nrest"; 158 } 159 } 160 } else { 161 $out[] = "$id\t$rest"; 162 } 163 } 164 165 return $out; 166} 167 168/** 169 * Event hook callback for AUTH_LOGIN_CHECK 170 * 171 * @param array $evdata 172 * @return bool 173 * @throws Exception 174 */ 175function auth_login_wrapper($evdata) 176{ 177 return auth_login( 178 $evdata['user'], 179 $evdata['password'], 180 $evdata['sticky'], 181 $evdata['silent'] 182 ); 183} 184 185/** 186 * This tries to login the user based on the sent auth credentials 187 * 188 * The authentication works like this: if a username was given 189 * a new login is assumed and user/password are checked. If they 190 * are correct the password is encrypted with blowfish and stored 191 * together with the username in a cookie - the same info is stored 192 * in the session, too. Additonally a browserID is stored in the 193 * session. 194 * 195 * If no username was given the cookie is checked: if the username, 196 * crypted password and browserID match between session and cookie 197 * no further testing is done and the user is accepted 198 * 199 * If a cookie was found but no session info was availabe the 200 * blowfish encrypted password from the cookie is decrypted and 201 * together with username rechecked by calling this function again. 202 * 203 * On a successful login $_SERVER[REMOTE_USER] and $USERINFO 204 * are set. 205 * 206 * @param string $user Username 207 * @param string $pass Cleartext Password 208 * @param bool $sticky Cookie should not expire 209 * @param bool $silent Don't show error on bad auth 210 * @return bool true on successful auth 211 * @throws Exception 212 * 213 * @author Andreas Gohr <andi@splitbrain.org> 214 */ 215function auth_login($user, $pass, $sticky = false, $silent = false) 216{ 217 global $USERINFO; 218 global $conf; 219 global $lang; 220 /* @var AuthPlugin $auth */ 221 global $auth; 222 /* @var Input $INPUT */ 223 global $INPUT; 224 225 if (!$auth instanceof AuthPlugin) return false; 226 227 if (!empty($user)) { 228 //usual login 229 if (!empty($pass) && $auth->checkPass($user, $pass)) { 230 // make logininfo globally available 231 $INPUT->server->set('REMOTE_USER', $user); 232 $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session 233 auth_setCookie($user, auth_encrypt($pass, $secret), $sticky); 234 return true; 235 } else { 236 //invalid credentials - log off 237 if (!$silent) { 238 http_status(403, 'Login failed'); 239 msg($lang['badlogin'], -1); 240 } 241 auth_logoff(); 242 return false; 243 } 244 } else { 245 // read cookie information 246 [$user, $sticky, $pass] = auth_getCookie(); 247 if ($user && $pass) { 248 // we got a cookie - see if we can trust it 249 250 // get session info 251 if (isset($_SESSION[DOKU_COOKIE])) { 252 $session = $_SESSION[DOKU_COOKIE]['auth']; 253 if ( 254 isset($session) && 255 $auth->useSessionCache($user) && 256 ($session['time'] >= time() - $conf['auth_security_timeout']) && 257 ($session['user'] == $user) && 258 ($session['pass'] == sha1($pass)) && //still crypted 259 ($session['buid'] == auth_browseruid()) 260 ) { 261 // he has session, cookie and browser right - let him in 262 $INPUT->server->set('REMOTE_USER', $user); 263 $USERINFO = $session['info']; //FIXME move all references to session 264 return true; 265 } 266 } 267 // no we don't trust it yet - recheck pass but silent 268 $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session 269 $pass = auth_decrypt($pass, $secret); 270 return auth_login($user, $pass, $sticky, true); 271 } 272 } 273 //just to be sure 274 auth_logoff(true); 275 return false; 276} 277 278/** 279 * Builds a pseudo UID from browser and IP data 280 * 281 * This is neither unique nor unfakable - still it adds some 282 * security. Using the first part of the IP makes sure 283 * proxy farms like AOLs are still okay. 284 * 285 * @author Andreas Gohr <andi@splitbrain.org> 286 * 287 * @return string a SHA256 sum of various browser headers 288 */ 289function auth_browseruid() 290{ 291 /* @var Input $INPUT */ 292 global $INPUT; 293 294 $ip = clientIP(true); 295 // convert IP string to packed binary representation 296 $pip = inet_pton($ip); 297 298 $uid = implode("\n", [ 299 $INPUT->server->str('HTTP_USER_AGENT'), 300 $INPUT->server->str('HTTP_ACCEPT_LANGUAGE'), 301 substr($pip, 0, strlen($pip) / 2), // use half of the IP address (works for both IPv4 and IPv6) 302 ]); 303 return hash('sha256', $uid); 304} 305 306/** 307 * Creates a random key to encrypt the password in cookies 308 * 309 * This function tries to read the password for encrypting 310 * cookies from $conf['metadir'].'/_htcookiesalt' 311 * if no such file is found a random key is created and 312 * and stored in this file. 313 * 314 * @param bool $addsession if true, the sessionid is added to the salt 315 * @param bool $secure if security is more important than keeping the old value 316 * @return string 317 * @throws Exception 318 * 319 * @author Andreas Gohr <andi@splitbrain.org> 320 */ 321function auth_cookiesalt($addsession = false, $secure = false) 322{ 323 if (defined('SIMPLE_TEST')) { 324 return 'test'; 325 } 326 global $conf; 327 $file = $conf['metadir'] . '/_htcookiesalt'; 328 if ($secure || !file_exists($file)) { 329 $file = $conf['metadir'] . '/_htcookiesalt2'; 330 } 331 $salt = io_readFile($file); 332 if (empty($salt)) { 333 $salt = bin2hex(auth_randombytes(64)); 334 io_saveFile($file, $salt); 335 } 336 if ($addsession) { 337 $salt .= session_id(); 338 } 339 return $salt; 340} 341 342/** 343 * Return cryptographically secure random bytes. 344 * 345 * @param int $length number of bytes 346 * @return string cryptographically secure random bytes 347 * @throws Exception 348 * 349 * @author Niklas Keller <me@kelunik.com> 350 */ 351function auth_randombytes($length) 352{ 353 return random_bytes($length); 354} 355 356/** 357 * Cryptographically secure random number generator. 358 * 359 * @param int $min 360 * @param int $max 361 * @return int 362 * @throws Exception 363 * 364 * @author Niklas Keller <me@kelunik.com> 365 */ 366function auth_random($min, $max) 367{ 368 return random_int($min, $max); 369} 370 371/** 372 * Encrypt data using the given secret using AES 373 * 374 * The mode is CBC with a random initialization vector, the key is derived 375 * using pbkdf2. 376 * 377 * @param string $data The data that shall be encrypted 378 * @param string $secret The secret/password that shall be used 379 * @return string The ciphertext 380 * @throws Exception 381 */ 382function auth_encrypt($data, $secret) 383{ 384 $iv = auth_randombytes(16); 385 $cipher = new AES(); 386 $cipher->setPassword($secret); 387 388 /* 389 this uses the encrypted IV as IV as suggested in 390 http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C 391 for unique but necessarily random IVs. The resulting ciphertext is 392 compatible to ciphertext that was created using a "normal" IV. 393 */ 394 return $cipher->encrypt($iv . $data); 395} 396 397/** 398 * Decrypt the given AES ciphertext 399 * 400 * The mode is CBC, the key is derived using pbkdf2 401 * 402 * @param string $ciphertext The encrypted data 403 * @param string $secret The secret/password that shall be used 404 * @return string The decrypted data 405 */ 406function auth_decrypt($ciphertext, $secret) 407{ 408 $iv = substr($ciphertext, 0, 16); 409 $cipher = new AES(); 410 $cipher->setPassword($secret); 411 $cipher->setIV($iv); 412 413 return $cipher->decrypt(substr($ciphertext, 16)); 414} 415 416/** 417 * Log out the current user 418 * 419 * This clears all authentication data and thus log the user 420 * off. It also clears session data. 421 * 422 * @author Andreas Gohr <andi@splitbrain.org> 423 * 424 * @param bool $keepbc - when true, the breadcrumb data is not cleared 425 */ 426function auth_logoff($keepbc = false) 427{ 428 global $conf; 429 global $USERINFO; 430 /* @var AuthPlugin $auth */ 431 global $auth; 432 /* @var Input $INPUT */ 433 global $INPUT; 434 435 // make sure the session is writable (it usually is) 436 @session_start(); 437 438 if (isset($_SESSION[DOKU_COOKIE]['auth']['user'])) 439 unset($_SESSION[DOKU_COOKIE]['auth']['user']); 440 if (isset($_SESSION[DOKU_COOKIE]['auth']['pass'])) 441 unset($_SESSION[DOKU_COOKIE]['auth']['pass']); 442 if (isset($_SESSION[DOKU_COOKIE]['auth']['info'])) 443 unset($_SESSION[DOKU_COOKIE]['auth']['info']); 444 if (!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc'])) 445 unset($_SESSION[DOKU_COOKIE]['bc']); 446 $INPUT->server->remove('REMOTE_USER'); 447 $USERINFO = null; //FIXME 448 449 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 450 setcookie(DOKU_COOKIE, '', [ 451 'expires' => time() - 600000, 452 'path' => $cookieDir, 453 'secure' => ($conf['securecookie'] && is_ssl()), 454 'httponly' => true, 455 'samesite' => $conf['samesitecookie'] ?: null, // null means browser default 456 ]); 457 458 if ($auth instanceof AuthPlugin) { 459 $auth->logOff(); 460 } 461} 462 463/** 464 * Check if a user is a manager 465 * 466 * Should usually be called without any parameters to check the current 467 * user. 468 * 469 * The info is available through $INFO['ismanager'], too 470 * 471 * @param string $user Username 472 * @param array $groups List of groups the user is in 473 * @param bool $adminonly when true checks if user is admin 474 * @param bool $recache set to true to refresh the cache 475 * @return bool 476 * @see auth_isadmin 477 * 478 * @author Andreas Gohr <andi@splitbrain.org> 479 */ 480function auth_ismanager($user = null, $groups = null, $adminonly = false, $recache = false) 481{ 482 global $conf; 483 global $USERINFO; 484 /* @var AuthPlugin $auth */ 485 global $auth; 486 /* @var Input $INPUT */ 487 global $INPUT; 488 489 490 if (!$auth instanceof AuthPlugin) return false; 491 if (is_null($user)) { 492 if (!$INPUT->server->has('REMOTE_USER')) { 493 return false; 494 } else { 495 $user = $INPUT->server->str('REMOTE_USER'); 496 } 497 } 498 if (is_null($groups)) { 499 // checking the logged in user, or another one? 500 if ($USERINFO && $user === $INPUT->server->str('REMOTE_USER')) { 501 $groups = (array) $USERINFO['grps']; 502 } else { 503 $groups = $auth->getUserData($user); 504 $groups = $groups ? $groups['grps'] : []; 505 } 506 } 507 508 // prefer cached result 509 static $cache = []; 510 $cachekey = serialize([$user, $adminonly, $groups]); 511 if (!isset($cache[$cachekey]) || $recache) { 512 // check superuser match 513 $ok = auth_isMember($conf['superuser'], $user, $groups); 514 515 // check managers 516 if (!$ok && !$adminonly) { 517 $ok = auth_isMember($conf['manager'], $user, $groups); 518 } 519 520 $cache[$cachekey] = $ok; 521 } 522 523 return $cache[$cachekey]; 524} 525 526/** 527 * Check if a user is admin 528 * 529 * Alias to auth_ismanager with adminonly=true 530 * 531 * The info is available through $INFO['isadmin'], too 532 * 533 * @param string $user Username 534 * @param array $groups List of groups the user is in 535 * @param bool $recache set to true to refresh the cache 536 * @return bool 537 * @author Andreas Gohr <andi@splitbrain.org> 538 * @see auth_ismanager() 539 * 540 */ 541function auth_isadmin($user = null, $groups = null, $recache = false) 542{ 543 return auth_ismanager($user, $groups, true, $recache); 544} 545 546/** 547 * Match a user and his groups against a comma separated list of 548 * users and groups to determine membership status 549 * 550 * Note: all input should NOT be nameencoded. 551 * 552 * @param string $memberlist commaseparated list of allowed users and groups 553 * @param string $user user to match against 554 * @param array $groups groups the user is member of 555 * @return bool true for membership acknowledged 556 */ 557function auth_isMember($memberlist, $user, array $groups) 558{ 559 /* @var AuthPlugin $auth */ 560 global $auth; 561 if (!$auth instanceof AuthPlugin) return false; 562 563 // clean user and groups 564 if (!$auth->isCaseSensitive()) { 565 $user = PhpString::strtolower($user); 566 $groups = array_map([PhpString::class, 'strtolower'], $groups); 567 } 568 $user = $auth->cleanUser($user); 569 $groups = array_map([$auth, 'cleanGroup'], $groups); 570 571 // extract the memberlist 572 $members = explode(',', $memberlist); 573 $members = array_map('trim', $members); 574 $members = array_unique($members); 575 $members = array_filter($members); 576 577 // compare cleaned values 578 foreach ($members as $member) { 579 if ($member == '@ALL') return true; 580 if (!$auth->isCaseSensitive()) $member = PhpString::strtolower($member); 581 if ($member[0] == '@') { 582 $member = $auth->cleanGroup(substr($member, 1)); 583 if (in_array($member, $groups)) return true; 584 } else { 585 $member = $auth->cleanUser($member); 586 if ($member == $user) return true; 587 } 588 } 589 590 // still here? not a member! 591 return false; 592} 593 594/** 595 * Convinience function for auth_aclcheck() 596 * 597 * This checks the permissions for the current user 598 * 599 * @author Andreas Gohr <andi@splitbrain.org> 600 * 601 * @param string $id page ID (needs to be resolved and cleaned) 602 * @return int permission level 603 */ 604function auth_quickaclcheck($id) 605{ 606 global $conf; 607 global $USERINFO; 608 /* @var Input $INPUT */ 609 global $INPUT; 610 # if no ACL is used always return upload rights 611 if (!$conf['useacl']) return AUTH_UPLOAD; 612 return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), is_array($USERINFO) ? $USERINFO['grps'] : []); 613} 614 615/** 616 * Returns the maximum rights a user has for the given ID or its namespace 617 * 618 * @author Andreas Gohr <andi@splitbrain.org> 619 * 620 * @triggers AUTH_ACL_CHECK 621 * @param string $id page ID (needs to be resolved and cleaned) 622 * @param string $user Username 623 * @param array|null $groups Array of groups the user is in 624 * @return int permission level 625 */ 626function auth_aclcheck($id, $user, $groups) 627{ 628 $data = [ 629 'id' => $id ?? '', 630 'user' => $user, 631 'groups' => $groups 632 ]; 633 634 return Event::createAndTrigger('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb'); 635} 636 637/** 638 * default ACL check method 639 * 640 * DO NOT CALL DIRECTLY, use auth_aclcheck() instead 641 * 642 * @author Andreas Gohr <andi@splitbrain.org> 643 * 644 * @param array $data event data 645 * @return int permission level 646 */ 647function auth_aclcheck_cb($data) 648{ 649 $id =& $data['id']; 650 $user =& $data['user']; 651 $groups =& $data['groups']; 652 653 global $conf; 654 global $AUTH_ACL; 655 /* @var AuthPlugin $auth */ 656 global $auth; 657 658 // if no ACL is used always return upload rights 659 if (!$conf['useacl']) return AUTH_UPLOAD; 660 if (!$auth instanceof AuthPlugin) return AUTH_NONE; 661 if (!is_array($AUTH_ACL)) return AUTH_NONE; 662 663 //make sure groups is an array 664 if (!is_array($groups)) $groups = []; 665 666 //if user is superuser or in superusergroup return 255 (acl_admin) 667 if (auth_isadmin($user, $groups)) { 668 return AUTH_ADMIN; 669 } 670 671 if (!$auth->isCaseSensitive()) { 672 $user = PhpString::strtolower($user); 673 $groups = array_map([PhpString::class, 'strtolower'], $groups); 674 } 675 $user = auth_nameencode($auth->cleanUser($user)); 676 $groups = array_map([$auth, 'cleanGroup'], $groups); 677 678 //prepend groups with @ and nameencode 679 foreach ($groups as &$group) { 680 $group = '@' . auth_nameencode($group); 681 } 682 683 $ns = getNS($id); 684 $perm = -1; 685 686 //add ALL group 687 $groups[] = '@ALL'; 688 689 //add User 690 if ($user) $groups[] = $user; 691 692 //check exact match first 693 $matches = preg_grep('/^' . preg_quote($id, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL); 694 if (count($matches)) { 695 foreach ($matches as $match) { 696 $match = preg_replace('/#.*$/', '', $match); //ignore comments 697 $acl = preg_split('/[ \t]+/', $match); 698 if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') { 699 $acl[1] = PhpString::strtolower($acl[1]); 700 } 701 if (!in_array($acl[1], $groups)) { 702 continue; 703 } 704 if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 705 if ($acl[2] > $perm) { 706 $perm = $acl[2]; 707 } 708 } 709 if ($perm > -1) { 710 //we had a match - return it 711 return (int) $perm; 712 } 713 } 714 715 //still here? do the namespace checks 716 if ($ns) { 717 $path = $ns . ':*'; 718 } else { 719 $path = '*'; //root document 720 } 721 722 do { 723 $matches = preg_grep('/^' . preg_quote($path, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL); 724 if (count($matches)) { 725 foreach ($matches as $match) { 726 $match = preg_replace('/#.*$/', '', $match); //ignore comments 727 $acl = preg_split('/[ \t]+/', $match); 728 if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') { 729 $acl[1] = PhpString::strtolower($acl[1]); 730 } 731 if (!in_array($acl[1], $groups)) { 732 continue; 733 } 734 if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL! 735 if ($acl[2] > $perm) { 736 $perm = $acl[2]; 737 } 738 } 739 //we had a match - return it 740 if ($perm != -1) { 741 return (int) $perm; 742 } 743 } 744 //get next higher namespace 745 $ns = getNS($ns); 746 747 if ($path != '*') { 748 $path = $ns . ':*'; 749 if ($path == ':*') $path = '*'; 750 } else { 751 //we did this already 752 //looks like there is something wrong with the ACL 753 //break here 754 msg('No ACL setup yet! Denying access to everyone.'); 755 return AUTH_NONE; 756 } 757 } while (1); //this should never loop endless 758 return AUTH_NONE; 759} 760 761/** 762 * Encode ASCII special chars 763 * 764 * Some auth backends allow special chars in their user and groupnames 765 * The special chars are encoded with this function. Only ASCII chars 766 * are encoded UTF-8 multibyte are left as is (different from usual 767 * urlencoding!). 768 * 769 * Decoding can be done with rawurldecode 770 * 771 * @author Andreas Gohr <gohr@cosmocode.de> 772 * @see rawurldecode() 773 * 774 * @param string $name 775 * @param bool $skip_group 776 * @return string 777 */ 778function auth_nameencode($name, $skip_group = false) 779{ 780 global $cache_authname; 781 $cache =& $cache_authname; 782 $name = (string) $name; 783 784 // never encode wildcard FS#1955 785 if ($name == '%USER%') return $name; 786 if ($name == '%GROUP%') return $name; 787 788 if (!isset($cache[$name][$skip_group])) { 789 if ($skip_group && $name[0] == '@') { 790 $cache[$name][$skip_group] = '@' . preg_replace_callback( 791 '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/', 792 'auth_nameencode_callback', 793 substr($name, 1) 794 ); 795 } else { 796 $cache[$name][$skip_group] = preg_replace_callback( 797 '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/', 798 'auth_nameencode_callback', 799 $name 800 ); 801 } 802 } 803 804 return $cache[$name][$skip_group]; 805} 806 807/** 808 * callback encodes the matches 809 * 810 * @param array $matches first complete match, next matching subpatterms 811 * @return string 812 */ 813function auth_nameencode_callback($matches) 814{ 815 return '%' . dechex(ord(substr($matches[1], -1))); 816} 817 818/** 819 * Create a pronouncable password 820 * 821 * The $foruser variable might be used by plugins to run additional password 822 * policy checks, but is not used by the default implementation 823 * 824 * @param string $foruser username for which the password is generated 825 * @return string pronouncable password 826 * @throws Exception 827 * 828 * @link http://www.phpbuilder.com/annotate/message.php3?id=1014451 829 * @triggers AUTH_PASSWORD_GENERATE 830 * 831 * @author Andreas Gohr <andi@splitbrain.org> 832 */ 833function auth_pwgen($foruser = '') 834{ 835 $data = [ 836 'password' => '', 837 'foruser' => $foruser 838 ]; 839 840 $evt = new Event('AUTH_PASSWORD_GENERATE', $data); 841 if ($evt->advise_before(true)) { 842 $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones 843 $v = 'aeiou'; //vowels 844 $a = $c . $v; //both 845 $s = '!$%&?+*~#-_:.;,'; // specials 846 847 //use thre syllables... 848 for ($i = 0; $i < 3; $i++) { 849 $data['password'] .= $c[auth_random(0, strlen($c) - 1)]; 850 $data['password'] .= $v[auth_random(0, strlen($v) - 1)]; 851 $data['password'] .= $a[auth_random(0, strlen($a) - 1)]; 852 } 853 //... and add a nice number and special 854 $data['password'] .= $s[auth_random(0, strlen($s) - 1)] . auth_random(10, 99); 855 } 856 $evt->advise_after(); 857 858 return $data['password']; 859} 860 861/** 862 * Sends a password to the given user 863 * 864 * @author Andreas Gohr <andi@splitbrain.org> 865 * 866 * @param string $user Login name of the user 867 * @param string $password The new password in clear text 868 * @return bool true on success 869 */ 870function auth_sendPassword($user, $password) 871{ 872 global $lang; 873 /* @var AuthPlugin $auth */ 874 global $auth; 875 if (!$auth instanceof AuthPlugin) return false; 876 877 $user = $auth->cleanUser($user); 878 $userinfo = $auth->getUserData($user, false); 879 880 if (!$userinfo['mail']) return false; 881 882 $text = rawLocale('password'); 883 $trep = [ 884 'FULLNAME' => $userinfo['name'], 885 'LOGIN' => $user, 886 'PASSWORD' => $password 887 ]; 888 889 $mail = new Mailer(); 890 $mail->to($mail->getCleanName($userinfo['name']) . ' <' . $userinfo['mail'] . '>'); 891 $mail->subject($lang['regpwmail']); 892 $mail->setBody($text, $trep); 893 return $mail->send(); 894} 895 896/** 897 * Register a new user 898 * 899 * This registers a new user - Data is read directly from $_POST 900 * 901 * @return bool true on success, false on any error 902 * @throws Exception 903 * 904 * @author Andreas Gohr <andi@splitbrain.org> 905 */ 906function register() 907{ 908 global $lang; 909 global $conf; 910 /* @var AuthPlugin $auth */ 911 global $auth; 912 global $INPUT; 913 914 if (!$INPUT->post->bool('save')) return false; 915 if (!actionOK('register')) return false; 916 917 // gather input 918 $login = trim($auth->cleanUser($INPUT->post->str('login'))); 919 $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname'))); 920 $email = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email'))); 921 $pass = $INPUT->post->str('pass'); 922 $passchk = $INPUT->post->str('passchk'); 923 924 if (empty($login) || empty($fullname) || empty($email)) { 925 msg($lang['regmissing'], -1); 926 return false; 927 } 928 929 if ($conf['autopasswd']) { 930 $pass = auth_pwgen($login); // automatically generate password 931 } elseif (empty($pass) || empty($passchk)) { 932 msg($lang['regmissing'], -1); // complain about missing passwords 933 return false; 934 } elseif ($pass != $passchk) { 935 msg($lang['regbadpass'], -1); // complain about misspelled passwords 936 return false; 937 } 938 939 //check mail 940 if (!mail_isvalid($email)) { 941 msg($lang['regbadmail'], -1); 942 return false; 943 } 944 945 //okay try to create the user 946 if (!$auth->triggerUserMod('create', [$login, $pass, $fullname, $email])) { 947 msg($lang['regfail'], -1); 948 return false; 949 } 950 951 // send notification about the new user 952 $subscription = new RegistrationSubscriptionSender(); 953 $subscription->sendRegister($login, $fullname, $email); 954 955 // are we done? 956 if (!$conf['autopasswd']) { 957 msg($lang['regsuccess2'], 1); 958 return true; 959 } 960 961 // autogenerated password? then send password to user 962 if (auth_sendPassword($login, $pass)) { 963 msg($lang['regsuccess'], 1); 964 return true; 965 } else { 966 msg($lang['regmailfail'], -1); 967 return false; 968 } 969} 970 971/** 972 * Update user profile 973 * 974 * @throws Exception 975 * 976 * @author Christopher Smith <chris@jalakai.co.uk> 977 */ 978function updateprofile() 979{ 980 global $conf; 981 global $lang; 982 /* @var AuthPlugin $auth */ 983 global $auth; 984 /* @var Input $INPUT */ 985 global $INPUT; 986 987 if (!$INPUT->post->bool('save')) return false; 988 if (!checkSecurityToken()) return false; 989 990 if (!actionOK('profile')) { 991 msg($lang['profna'], -1); 992 return false; 993 } 994 995 $changes = []; 996 $changes['pass'] = $INPUT->post->str('newpass'); 997 $changes['name'] = $INPUT->post->str('fullname'); 998 $changes['mail'] = $INPUT->post->str('email'); 999 1000 // check misspelled passwords 1001 if ($changes['pass'] != $INPUT->post->str('passchk')) { 1002 msg($lang['regbadpass'], -1); 1003 return false; 1004 } 1005 1006 // clean fullname and email 1007 $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name'])); 1008 $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail'])); 1009 1010 // no empty name and email (except the backend doesn't support them) 1011 if ( 1012 (empty($changes['name']) && $auth->canDo('modName')) || 1013 (empty($changes['mail']) && $auth->canDo('modMail')) 1014 ) { 1015 msg($lang['profnoempty'], -1); 1016 return false; 1017 } 1018 if (!mail_isvalid($changes['mail']) && $auth->canDo('modMail')) { 1019 msg($lang['regbadmail'], -1); 1020 return false; 1021 } 1022 1023 $changes = array_filter($changes); 1024 1025 // check for unavailable capabilities 1026 if (!$auth->canDo('modName')) unset($changes['name']); 1027 if (!$auth->canDo('modMail')) unset($changes['mail']); 1028 if (!$auth->canDo('modPass')) unset($changes['pass']); 1029 1030 // anything to do? 1031 if ($changes === []) { 1032 msg($lang['profnochange'], -1); 1033 return false; 1034 } 1035 1036 if ($conf['profileconfirm']) { 1037 if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) { 1038 msg($lang['badpassconfirm'], -1); 1039 return false; 1040 } 1041 } 1042 1043 if (!$auth->triggerUserMod('modify', [$INPUT->server->str('REMOTE_USER'), &$changes])) { 1044 msg($lang['proffail'], -1); 1045 return false; 1046 } 1047 1048 if ($changes['pass']) { 1049 // update cookie and session with the changed data 1050 [/* user */, $sticky, /* pass */] = auth_getCookie(); 1051 $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true)); 1052 auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky); 1053 } else { 1054 // make sure the session is writable 1055 @session_start(); 1056 // invalidate session cache 1057 $_SESSION[DOKU_COOKIE]['auth']['time'] = 0; 1058 session_write_close(); 1059 } 1060 1061 return true; 1062} 1063 1064/** 1065 * Delete the current logged-in user 1066 * 1067 * @return bool true on success, false on any error 1068 */ 1069function auth_deleteprofile() 1070{ 1071 global $conf; 1072 global $lang; 1073 /* @var AuthPlugin $auth */ 1074 global $auth; 1075 /* @var Input $INPUT */ 1076 global $INPUT; 1077 1078 if (!$INPUT->post->bool('delete')) return false; 1079 if (!checkSecurityToken()) return false; 1080 1081 // action prevented or auth module disallows 1082 if (!actionOK('profile_delete') || !$auth->canDo('delUser')) { 1083 msg($lang['profnodelete'], -1); 1084 return false; 1085 } 1086 1087 if (!$INPUT->post->bool('confirm_delete')) { 1088 msg($lang['profconfdeletemissing'], -1); 1089 return false; 1090 } 1091 1092 if ($conf['profileconfirm']) { 1093 if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) { 1094 msg($lang['badpassconfirm'], -1); 1095 return false; 1096 } 1097 } 1098 1099 $deleted = []; 1100 $deleted[] = $INPUT->server->str('REMOTE_USER'); 1101 if ($auth->triggerUserMod('delete', [$deleted])) { 1102 // force and immediate logout including removing the sticky cookie 1103 auth_logoff(); 1104 return true; 1105 } 1106 1107 return false; 1108} 1109 1110/** 1111 * Send a new password 1112 * 1113 * This function handles both phases of the password reset: 1114 * 1115 * - handling the first request of password reset 1116 * - validating the password reset auth token 1117 * 1118 * @return bool true on success, false on any error 1119 * @throws Exception 1120 * 1121 * @author Andreas Gohr <andi@splitbrain.org> 1122 * @author Benoit Chesneau <benoit@bchesneau.info> 1123 * @author Chris Smith <chris@jalakai.co.uk> 1124 */ 1125function act_resendpwd() 1126{ 1127 global $lang; 1128 global $conf; 1129 /* @var AuthPlugin $auth */ 1130 global $auth; 1131 /* @var Input $INPUT */ 1132 global $INPUT; 1133 1134 if (!actionOK('resendpwd')) { 1135 msg($lang['resendna'], -1); 1136 return false; 1137 } 1138 1139 $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth')); 1140 1141 if ($token) { 1142 // we're in token phase - get user info from token 1143 1144 $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth'; 1145 if (!file_exists($tfile)) { 1146 msg($lang['resendpwdbadauth'], -1); 1147 $INPUT->remove('pwauth'); 1148 return false; 1149 } 1150 // token is only valid for 3 days 1151 if ((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) { 1152 msg($lang['resendpwdbadauth'], -1); 1153 $INPUT->remove('pwauth'); 1154 @unlink($tfile); 1155 return false; 1156 } 1157 1158 $user = io_readfile($tfile); 1159 $userinfo = $auth->getUserData($user, false); 1160 if (!$userinfo['mail']) { 1161 msg($lang['resendpwdnouser'], -1); 1162 return false; 1163 } 1164 1165 if (!$conf['autopasswd']) { // we let the user choose a password 1166 $pass = $INPUT->str('pass'); 1167 1168 // password given correctly? 1169 if (!$pass) return false; 1170 if ($pass != $INPUT->str('passchk')) { 1171 msg($lang['regbadpass'], -1); 1172 return false; 1173 } 1174 1175 // change it 1176 if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) { 1177 msg($lang['proffail'], -1); 1178 return false; 1179 } 1180 } else { // autogenerate the password and send by mail 1181 $pass = auth_pwgen($user); 1182 if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) { 1183 msg($lang['proffail'], -1); 1184 return false; 1185 } 1186 1187 if (auth_sendPassword($user, $pass)) { 1188 msg($lang['resendpwdsuccess'], 1); 1189 } else { 1190 msg($lang['regmailfail'], -1); 1191 } 1192 } 1193 1194 @unlink($tfile); 1195 return true; 1196 } else { 1197 // we're in request phase 1198 1199 if (!$INPUT->post->bool('save')) return false; 1200 1201 if (!$INPUT->post->str('login')) { 1202 msg($lang['resendpwdmissing'], -1); 1203 return false; 1204 } else { 1205 $user = trim($auth->cleanUser($INPUT->post->str('login'))); 1206 } 1207 1208 $userinfo = $auth->getUserData($user, false); 1209 if (!$userinfo['mail']) { 1210 msg($lang['resendpwdnouser'], -1); 1211 return false; 1212 } 1213 1214 // generate auth token 1215 $token = md5(auth_randombytes(16)); // random secret 1216 $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth'; 1217 $url = wl('', ['do' => 'resendpwd', 'pwauth' => $token], true, '&'); 1218 1219 io_saveFile($tfile, $user); 1220 1221 $text = rawLocale('pwconfirm'); 1222 $trep = ['FULLNAME' => $userinfo['name'], 'LOGIN' => $user, 'CONFIRM' => $url]; 1223 1224 $mail = new Mailer(); 1225 $mail->to($userinfo['name'] . ' <' . $userinfo['mail'] . '>'); 1226 $mail->subject($lang['regpwmail']); 1227 $mail->setBody($text, $trep); 1228 if ($mail->send()) { 1229 msg($lang['resendpwdconfirm'], 1); 1230 } else { 1231 msg($lang['regmailfail'], -1); 1232 } 1233 return true; 1234 } 1235 // never reached 1236} 1237 1238/** 1239 * Encrypts a password using the given method and salt 1240 * 1241 * If the selected method needs a salt and none was given, a random one 1242 * is chosen. 1243 * 1244 * @author Andreas Gohr <andi@splitbrain.org> 1245 * 1246 * @param string $clear The clear text password 1247 * @param string $method The hashing method 1248 * @param string $salt A salt, null for random 1249 * @return string The crypted password 1250 */ 1251function auth_cryptPassword($clear, $method = '', $salt = null) 1252{ 1253 global $conf; 1254 if (empty($method)) $method = $conf['passcrypt']; 1255 1256 $pass = new PassHash(); 1257 $call = 'hash_' . $method; 1258 1259 if (!method_exists($pass, $call)) { 1260 msg("Unsupported crypt method $method", -1); 1261 return false; 1262 } 1263 1264 return $pass->$call($clear, $salt); 1265} 1266 1267/** 1268 * Verifies a cleartext password against a crypted hash 1269 * 1270 * @param string $clear The clear text password 1271 * @param string $crypt The hash to compare with 1272 * @return bool true if both match 1273 * @throws Exception 1274 * 1275 * @author Andreas Gohr <andi@splitbrain.org> 1276 */ 1277function auth_verifyPassword($clear, $crypt) 1278{ 1279 $pass = new PassHash(); 1280 return $pass->verify_hash($clear, $crypt); 1281} 1282 1283/** 1284 * Set the authentication cookie and add user identification data to the session 1285 * 1286 * @param string $user username 1287 * @param string $pass encrypted password 1288 * @param bool $sticky whether or not the cookie will last beyond the session 1289 * @return bool 1290 */ 1291function auth_setCookie($user, $pass, $sticky) 1292{ 1293 global $conf; 1294 /* @var AuthPlugin $auth */ 1295 global $auth; 1296 global $USERINFO; 1297 1298 if (!$auth instanceof AuthPlugin) return false; 1299 $USERINFO = $auth->getUserData($user); 1300 1301 // set cookie 1302 $cookie = base64_encode($user) . '|' . ((int) $sticky) . '|' . base64_encode($pass); 1303 $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir']; 1304 $time = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year 1305 setcookie(DOKU_COOKIE, $cookie, [ 1306 'expires' => $time, 1307 'path' => $cookieDir, 1308 'secure' => ($conf['securecookie'] && is_ssl()), 1309 'httponly' => true, 1310 'samesite' => $conf['samesitecookie'] ?: null, // null means browser default 1311 ]); 1312 1313 // set session 1314 $_SESSION[DOKU_COOKIE]['auth']['user'] = $user; 1315 $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass); 1316 $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid(); 1317 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 1318 $_SESSION[DOKU_COOKIE]['auth']['time'] = time(); 1319 1320 return true; 1321} 1322 1323/** 1324 * Returns the user, (encrypted) password and sticky bit from cookie 1325 * 1326 * @returns array 1327 */ 1328function auth_getCookie() 1329{ 1330 if (!isset($_COOKIE[DOKU_COOKIE])) { 1331 return [null, null, null]; 1332 } 1333 [$user, $sticky, $pass] = sexplode('|', $_COOKIE[DOKU_COOKIE], 3, ''); 1334 $sticky = (bool) $sticky; 1335 $pass = base64_decode($pass); 1336 $user = base64_decode($user); 1337 return [$user, $sticky, $pass]; 1338} 1339 1340//Setup VIM: ex: et ts=2 : 1341