PageRenderTime 63ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/auth.php

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