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