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