xref: /dokuwiki/inc/auth.php (revision 430db121ade4ad520e7c2e0518bc0fb4fd969484)
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 auth_basic $auth */
38    global $auth;
39    /* @var Input $INPUT */
40    global $INPUT;
41    global $AUTH_ACL;
42    global $lang;
43    $AUTH_ACL = array();
44
45    if(!$conf['useacl']) return false;
46
47    // load the the backend auth functions and instantiate the auth object XXX
48    if(@file_exists(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php')) {
49        require_once(DOKU_INC.'inc/auth/basic.class.php');
50        require_once(DOKU_INC.'inc/auth/'.$conf['authtype'].'.class.php');
51
52        $auth_class = "auth_".$conf['authtype'];
53        if(class_exists($auth_class)) {
54            $auth = new $auth_class();
55            if($auth->success == false) {
56                // degrade to unauthenticated user
57                unset($auth);
58                auth_logoff();
59                msg($lang['authtempfail'], -1);
60            }
61        } else {
62            nice_die($lang['authmodfailed']);
63        }
64    } else {
65        nice_die($lang['authmodfailed']);
66    }
67
68    if(!$auth) return false;
69
70    // do the login either by cookie or provided credentials XXX
71    $INPUT->set('http_credentials', false);
72    if(!$conf['rememberme']) $INPUT->set('r', false);
73
74    // handle renamed HTTP_AUTHORIZATION variable (can happen when a fix like
75    // the one presented at
76    // http://www.besthostratings.com/articles/http-auth-php-cgi.html is used
77    // for enabling HTTP authentication with CGI/SuExec)
78    if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
79        $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
80    // streamline HTTP auth credentials (IIS/rewrite -> mod_php)
81    if(isset($_SERVER['HTTP_AUTHORIZATION'])) {
82        list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
83            explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
84    }
85
86    // if no credentials were given try to use HTTP auth (for SSO)
87    if(!$INPUT->str('u') && empty($_COOKIE[DOKU_COOKIE]) && !empty($_SERVER['PHP_AUTH_USER'])) {
88        $INPUT->set('u', $_SERVER['PHP_AUTH_USER']);
89        $INPUT->set('p', $_SERVER['PHP_AUTH_PW']);
90        $INPUT->set('http_credentials', true);
91    }
92
93    // apply cleaning
94    $INPUT->set('u', $auth->cleanUser($INPUT->str('u')));
95
96    if($INPUT->str('authtok')) {
97        // when an authentication token is given, trust the session
98        auth_validateToken($INPUT->str('authtok'));
99    } elseif(!is_null($auth) && $auth->canDo('external')) {
100        // external trust mechanism in place
101        $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r'));
102    } else {
103        $evdata = array(
104            'user'     => $INPUT->str('u'),
105            'password' => $INPUT->str('p'),
106            'sticky'   => $INPUT->bool('r'),
107            'silent'   => $INPUT->bool('http_credentials')
108        );
109        trigger_event('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
110    }
111
112    //load ACL into a global array XXX
113    $AUTH_ACL = auth_loadACL();
114
115    return true;
116}
117
118/**
119 * Loads the ACL setup and handle user wildcards
120 *
121 * @author Andreas Gohr <andi@splitbrain.org>
122 * @return array
123 */
124function auth_loadACL() {
125    global $config_cascade;
126    global $USERINFO;
127
128    if(!is_readable($config_cascade['acl']['default'])) return array();
129
130    $acl = file($config_cascade['acl']['default']);
131
132    //support user wildcard
133    if(isset($_SERVER['REMOTE_USER'])){
134        $len = count($acl);
135        for($i = 0; $i < $len; $i++) {
136            if($acl[$i]{0} == '#') continue;
137            list($id,$rest) = preg_split('/\s+/',$acl[$i],2);
138            if(strstr($acl[$i], '%GROUP%')){
139                foreach($USERINFO['grps'] as $grp){
140                    $nid   = str_replace('%GROUP%',cleanID($grp),$id);
141                    $nrest = str_replace('%GROUP%',auth_nameencode($grp),$rest);
142                    $acl[] = "$nid\t$nrest";
143                }
144            }
145            $id   = str_replace('%USER%',cleanID($_SERVER['REMOTE_USER']),$id);
146            $rest = str_replace('%USER%',auth_nameencode($_SERVER['REMOTE_USER']),$rest);
147            $acl[$i] = "$id\t$rest";
148        }
149    }
150    return $acl;
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
735    if(!$_POST['save']) return false;
736    if(!actionOK('register')) return false;
737
738    //clean username
739    $_POST['login'] = trim($auth->cleanUser($_POST['login']));
740
741    //clean fullname and email
742    $_POST['fullname'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $_POST['fullname']));
743    $_POST['email']    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $_POST['email']));
744
745    if(empty($_POST['login']) ||
746        empty($_POST['fullname']) ||
747        empty($_POST['email'])
748    ) {
749        msg($lang['regmissing'], -1);
750        return false;
751    }
752
753    if($conf['autopasswd']) {
754        $pass = auth_pwgen(); // automatically generate password
755    } elseif(empty($_POST['pass']) ||
756        empty($_POST['passchk'])
757    ) {
758        msg($lang['regmissing'], -1); // complain about missing passwords
759        return false;
760    } elseif($_POST['pass'] != $_POST['passchk']) {
761        msg($lang['regbadpass'], -1); // complain about misspelled passwords
762        return false;
763    } else {
764        $pass = $_POST['pass']; // accept checked and valid password
765    }
766
767    //check mail
768    if(!mail_isvalid($_POST['email'])) {
769        msg($lang['regbadmail'], -1);
770        return false;
771    }
772
773    //okay try to create the user
774    if(!$auth->triggerUserMod('create', array($_POST['login'], $pass, $_POST['fullname'], $_POST['email']))) {
775        msg($lang['reguexists'], -1);
776        return false;
777    }
778
779    // create substitutions for use in notification email
780    $substitutions = array(
781        'NEWUSER'  => $_POST['login'],
782        'NEWNAME'  => $_POST['fullname'],
783        'NEWEMAIL' => $_POST['email'],
784    );
785
786    if(!$conf['autopasswd']) {
787        msg($lang['regsuccess2'], 1);
788        notify('', 'register', '', $_POST['login'], false, $substitutions);
789        return true;
790    }
791
792    // autogenerated password? then send him the password
793    if(auth_sendPassword($_POST['login'], $pass)) {
794        msg($lang['regsuccess'], 1);
795        notify('', 'register', '', $_POST['login'], false, $substitutions);
796        return true;
797    } else {
798        msg($lang['regmailfail'], -1);
799        return false;
800    }
801}
802
803/**
804 * Update user profile
805 *
806 * @author    Christopher Smith <chris@jalakai.co.uk>
807 */
808function updateprofile() {
809    global $conf;
810    global $lang;
811    /* @var auth_basic $auth */
812    global $auth;
813    /* @var Input $INPUT */
814    global $INPUT;
815
816    if(!$INPUT->post->bool('save')) return false;
817    if(!checkSecurityToken()) return false;
818
819    if(!actionOK('profile')) {
820        msg($lang['profna'], -1);
821        return false;
822    }
823
824    $changes         = array();
825    $changes['pass'] = $INPUT->post->str('newpass');
826    $changes['name'] = $INPUT->post->str('fullname');
827    $changes['mail'] = $INPUT->post->str('email');
828
829    // check misspelled passwords
830    if($changes['pass'] != $INPUT->post->str('passchk')) {
831        msg($lang['regbadpass'], -1);
832        return false;
833    }
834
835    // clean fullname and email
836    $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
837    $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
838
839    // no empty name and email (except the backend doesn't support them)
840    if((empty($changes['name']) && $auth->canDo('modName')) ||
841        (empty($changes['mail']) && $auth->canDo('modMail'))
842    ) {
843        msg($lang['profnoempty'], -1);
844        return false;
845    }
846    if(!mail_isvalid($changes['mail']) && $auth->canDo('modMail')) {
847        msg($lang['regbadmail'], -1);
848        return false;
849    }
850
851    $changes = array_filter($changes);
852
853    // check for unavailable capabilities
854    if(!$auth->canDo('modName')) unset($changes['name']);
855    if(!$auth->canDo('modMail')) unset($changes['mail']);
856    if(!$auth->canDo('modPass')) unset($changes['pass']);
857
858    // anything to do?
859    if(!count($changes)) {
860        msg($lang['profnochange'], -1);
861        return false;
862    }
863
864    if($conf['profileconfirm']) {
865        if(!$auth->checkPass($_SERVER['REMOTE_USER'], $INPUT->post->str('oldpass'))) {
866            msg($lang['badlogin'], -1);
867            return false;
868        }
869    }
870
871    if($result = $auth->triggerUserMod('modify', array($_SERVER['REMOTE_USER'], $changes))) {
872        // update cookie and session with the changed data
873        if($changes['pass']) {
874            list( /*user*/, $sticky, /*pass*/) = auth_getCookie();
875            $pass = PMA_blowfish_encrypt($changes['pass'], auth_cookiesalt(!$sticky));
876            auth_setCookie($_SERVER['REMOTE_USER'], $pass, (bool) $sticky);
877        }
878        return true;
879    }
880
881    return false;
882}
883
884/**
885 * Send a  new password
886 *
887 * This function handles both phases of the password reset:
888 *
889 *   - handling the first request of password reset
890 *   - validating the password reset auth token
891 *
892 * @author Benoit Chesneau <benoit@bchesneau.info>
893 * @author Chris Smith <chris@jalakai.co.uk>
894 * @author Andreas Gohr <andi@splitbrain.org>
895 *
896 * @return bool true on success, false on any error
897 */
898function act_resendpwd() {
899    global $lang;
900    global $conf;
901    /* @var auth_basic $auth */
902    global $auth;
903    /* @var Input $INPUT */
904    global $INPUT;
905
906    if(!actionOK('resendpwd')) {
907        msg($lang['resendna'], -1);
908        return false;
909    }
910
911    $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
912
913    if($token) {
914        // we're in token phase - get user info from token
915
916        $tfile = $conf['cachedir'].'/'.$token{0}.'/'.$token.'.pwauth';
917        if(!@file_exists($tfile)) {
918            msg($lang['resendpwdbadauth'], -1);
919            $INPUT->remove('pwauth');
920            return false;
921        }
922        // token is only valid for 3 days
923        if((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
924            msg($lang['resendpwdbadauth'], -1);
925            $INPUT->remove('pwauth');
926            @unlink($tfile);
927            return false;
928        }
929
930        $user     = io_readfile($tfile);
931        $userinfo = $auth->getUserData($user);
932        if(!$userinfo['mail']) {
933            msg($lang['resendpwdnouser'], -1);
934            return false;
935        }
936
937        if(!$conf['autopasswd']) { // we let the user choose a password
938            $pass = $INPUT->str('pass');
939
940            // password given correctly?
941            if(!$pass) return false;
942            if($pass != $INPUT->str('passchk')) {
943                msg($lang['regbadpass'], -1);
944                return false;
945            }
946
947            // change it
948            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
949                msg('error modifying user data', -1);
950                return false;
951            }
952
953        } else { // autogenerate the password and send by mail
954
955            $pass = auth_pwgen();
956            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
957                msg('error modifying user data', -1);
958                return false;
959            }
960
961            if(auth_sendPassword($user, $pass)) {
962                msg($lang['resendpwdsuccess'], 1);
963            } else {
964                msg($lang['regmailfail'], -1);
965            }
966        }
967
968        @unlink($tfile);
969        return true;
970
971    } else {
972        // we're in request phase
973
974        if(!$INPUT->post->bool('save')) return false;
975
976        if(!$INPUT->post->str('login')) {
977            msg($lang['resendpwdmissing'], -1);
978            return false;
979        } else {
980            $user = trim($auth->cleanUser($INPUT->post->str('login')));
981        }
982
983        $userinfo = $auth->getUserData($user);
984        if(!$userinfo['mail']) {
985            msg($lang['resendpwdnouser'], -1);
986            return false;
987        }
988
989        // generate auth token
990        $token = md5(auth_cookiesalt().$user); //secret but user based
991        $tfile = $conf['cachedir'].'/'.$token{0}.'/'.$token.'.pwauth';
992        $url   = wl('', array('do'=> 'resendpwd', 'pwauth'=> $token), true, '&');
993
994        io_saveFile($tfile, $user);
995
996        $text = rawLocale('pwconfirm');
997        $trep = array(
998            'FULLNAME' => $userinfo['name'],
999            'LOGIN'    => $user,
1000            'CONFIRM'  => $url
1001        );
1002
1003        $mail = new Mailer();
1004        $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>');
1005        $mail->subject($lang['regpwmail']);
1006        $mail->setBody($text, $trep);
1007        if($mail->send()) {
1008            msg($lang['resendpwdconfirm'], 1);
1009        } else {
1010            msg($lang['regmailfail'], -1);
1011        }
1012        return true;
1013    }
1014    // never reached
1015}
1016
1017/**
1018 * Encrypts a password using the given method and salt
1019 *
1020 * If the selected method needs a salt and none was given, a random one
1021 * is chosen.
1022 *
1023 * @author  Andreas Gohr <andi@splitbrain.org>
1024 * @param string $clear The clear text password
1025 * @param string $method The hashing method
1026 * @param string $salt A salt, null for random
1027 * @return  string  The crypted password
1028 */
1029function auth_cryptPassword($clear, $method = '', $salt = null) {
1030    global $conf;
1031    if(empty($method)) $method = $conf['passcrypt'];
1032
1033    $pass = new PassHash();
1034    $call = 'hash_'.$method;
1035
1036    if(!method_exists($pass, $call)) {
1037        msg("Unsupported crypt method $method", -1);
1038        return false;
1039    }
1040
1041    return $pass->$call($clear, $salt);
1042}
1043
1044/**
1045 * Verifies a cleartext password against a crypted hash
1046 *
1047 * @author Andreas Gohr <andi@splitbrain.org>
1048 * @param  string $clear The clear text password
1049 * @param  string $crypt The hash to compare with
1050 * @return bool true if both match
1051 */
1052function auth_verifyPassword($clear, $crypt) {
1053    $pass = new PassHash();
1054    return $pass->verify_hash($clear, $crypt);
1055}
1056
1057/**
1058 * Set the authentication cookie and add user identification data to the session
1059 *
1060 * @param string  $user       username
1061 * @param string  $pass       encrypted password
1062 * @param bool    $sticky     whether or not the cookie will last beyond the session
1063 * @return bool
1064 */
1065function auth_setCookie($user, $pass, $sticky) {
1066    global $conf;
1067    /* @var auth_basic $auth */
1068    global $auth;
1069    global $USERINFO;
1070
1071    if(!$auth) return false;
1072    $USERINFO = $auth->getUserData($user);
1073
1074    // set cookie
1075    $cookie    = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode($pass);
1076    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1077    $time      = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
1078    if(version_compare(PHP_VERSION, '5.2.0', '>')) {
1079        setcookie(DOKU_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
1080    } else {
1081        setcookie(DOKU_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()));
1082    }
1083    // set session
1084    $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
1085    $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
1086    $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
1087    $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
1088    $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
1089
1090    return true;
1091}
1092
1093/**
1094 * Returns the user, (encrypted) password and sticky bit from cookie
1095 *
1096 * @returns array
1097 */
1098function auth_getCookie() {
1099    if(!isset($_COOKIE[DOKU_COOKIE])) {
1100        return array(null, null, null);
1101    }
1102    list($user, $sticky, $pass) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
1103    $sticky = (bool) $sticky;
1104    $pass   = base64_decode($pass);
1105    $user   = base64_decode($user);
1106    return array($user, $sticky, $pass);
1107}
1108
1109//Setup VIM: ex: et ts=2 :
1110