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