Failed Conditions
Push — tokenauth ( b0ac60 )
by Andreas
08:12
created

inc/auth.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 <[email protected]>
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 DokuWiki_Auth_Plugin $auth */
38
    global $auth;
39
    /* @var Input $INPUT */
40
    global $INPUT;
41
    global $AUTH_ACL;
42
    global $lang;
43
    /* @var Doku_Plugin_Controller $plugin_controller */
44
    global $plugin_controller;
45
    $AUTH_ACL = array();
46
47
    if(!$conf['useacl']) return false;
48
49
    // try to load auth backend from plugins
50
    foreach ($plugin_controller->getList('auth') as $plugin) {
51
        if ($conf['authtype'] === $plugin) {
52
            $auth = $plugin_controller->load('auth', $plugin);
53
            break;
54
        }
55
    }
56
57
    if(!isset($auth) || !$auth){
58
        msg($lang['authtempfail'], -1);
59
        return false;
60
    }
61
62
    if ($auth->success == false) {
63
        // degrade to unauthenticated user
64
        unset($auth);
65
        auth_logoff();
66
        msg($lang['authtempfail'], -1);
67
        return false;
68
    }
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 (auth specific user names, remove control chars)
94
    if (true === $auth->success) {
95
        $INPUT->set('u', $auth->cleanUser(stripctl($INPUT->str('u'))));
96
        $INPUT->set('p', stripctl($INPUT->str('p')));
97
    }
98
99
    // do the login
100
    if(!auth_tokenlogin()) {
101
        if(!is_null($auth) && $auth->canDo('external')) {
102
            // external trust mechanism in place
103
            $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r'));
104
        } else {
105
            $evdata = array(
106
                'user' => $INPUT->str('u'),
107
                'password' => $INPUT->str('p'),
108
                'sticky' => $INPUT->bool('r'),
109
                'silent' => $INPUT->bool('http_credentials')
110
            );
111
            trigger_event('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
112
        }
113
    }
114
115
    //load ACL into a global array XXX
116
    $AUTH_ACL = auth_loadACL();
117
118
    return true;
119
}
120
121
/**
122
 * Loads the ACL setup and handle user wildcards
123
 *
124
 * @author Andreas Gohr <[email protected]>
125
 *
126
 * @return array
127
 */
128
function auth_loadACL() {
129
    global $config_cascade;
130
    global $USERINFO;
131
    /* @var Input $INPUT */
132
    global $INPUT;
133
134
    if(!is_readable($config_cascade['acl']['default'])) return array();
135
136
    $acl = file($config_cascade['acl']['default']);
137
138
    $out = array();
139
    foreach($acl as $line) {
140
        $line = trim($line);
141
        if(empty($line) || ($line{0} == '#')) continue; // skip blank lines & comments
142
        list($id,$rest) = preg_split('/[ \t]+/',$line,2);
143
144
        // substitute user wildcard first (its 1:1)
145
        if(strstr($line, '%USER%')){
146
            // if user is not logged in, this ACL line is meaningless - skip it
147
            if (!$INPUT->server->has('REMOTE_USER')) continue;
148
149
            $id   = str_replace('%USER%',cleanID($INPUT->server->str('REMOTE_USER')),$id);
150
            $rest = str_replace('%USER%',auth_nameencode($INPUT->server->str('REMOTE_USER')),$rest);
151
        }
152
153
        // substitute group wildcard (its 1:m)
154
        if(strstr($line, '%GROUP%')){
155
            // if user is not logged in, grps is empty, no output will be added (i.e. skipped)
156
            foreach((array) $USERINFO['grps'] as $grp){
157
                $nid   = str_replace('%GROUP%',cleanID($grp),$id);
158
                $nrest = str_replace('%GROUP%','@'.auth_nameencode($grp),$rest);
159
                $out[] = "$nid\t$nrest";
160
            }
161
        } else {
162
            $out[] = "$id\t$rest";
163
        }
164
    }
165
166
    return $out;
167
}
168
169
/**
170
 * Try a token login
171
 *
172
 * @return bool true if token login succeeded
173
 */
174
function auth_tokenlogin() {
175
    global $USERINFO;
176
    global $INPUT;
177
    /** @var DokuWiki_Auth_Plugin $auth */
178
    global $auth;
179
    if(!$auth) return false;
180
181
    // see if header has token
182
    $header = '';
183
    if(function_exists('apache_request_headers')) {
184
        // Authorization headers are not in $_SERVER for mod_php
185
        $headers = apache_request_headers();
186
        if(isset($headers['Authorization'])) $header = $headers['Authorization'];
187
    } else {
188
        $header = $INPUT->server->str('HTTP_AUTHORIZATION');
189
    }
190
    if(!$header) return false;
191
    list($type, $token) = explode(' ', $header, 2);
192
    if($type !== 'DokuWiki') return false;
193
194
    // check token
195
    $authtoken = \dokuwiki\AuthenticationToken::fromToken($token);
196
    if(!$authtoken->check($token)) return false;
197
198
    // fetch user info from backend
199
    $user = $authtoken->getUser();
200
    $USERINFO = $auth->getUserData($user);
0 ignored issues
show
It seems like $user defined by $authtoken->getUser() on line 199 can also be of type false or null; however, DokuWiki_Auth_Plugin::getUserData() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
201
    if(!$USERINFO) return false;
202
203
    // the code is correct, set up user
204
    $INPUT->server->set('REMOTE_USER', $user);
205
    $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
206
    $_SESSION[DOKU_COOKIE]['auth']['pass'] = 'nope';
207
    $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
208
209
    return true;
210
}
211
212
/**
213
 * Event hook callback for AUTH_LOGIN_CHECK
214
 *
215
 * @param array $evdata
216
 * @return bool
217
 */
218
function auth_login_wrapper($evdata) {
219
    return auth_login(
220
        $evdata['user'],
221
        $evdata['password'],
222
        $evdata['sticky'],
223
        $evdata['silent']
224
    );
225
}
226
227
/**
228
 * This tries to login the user based on the sent auth credentials
229
 *
230
 * The authentication works like this: if a username was given
231
 * a new login is assumed and user/password are checked. If they
232
 * are correct the password is encrypted with blowfish and stored
233
 * together with the username in a cookie - the same info is stored
234
 * in the session, too. Additonally a browserID is stored in the
235
 * session.
236
 *
237
 * If no username was given the cookie is checked: if the username,
238
 * crypted password and browserID match between session and cookie
239
 * no further testing is done and the user is accepted
240
 *
241
 * If a cookie was found but no session info was availabe the
242
 * blowfish encrypted password from the cookie is decrypted and
243
 * together with username rechecked by calling this function again.
244
 *
245
 * On a successful login $_SERVER[REMOTE_USER] and $USERINFO
246
 * are set.
247
 *
248
 * @author  Andreas Gohr <[email protected]>
249
 *
250
 * @param   string  $user    Username
251
 * @param   string  $pass    Cleartext Password
252
 * @param   bool    $sticky  Cookie should not expire
253
 * @param   bool    $silent  Don't show error on bad auth
254
 * @return  bool             true on successful auth
255
 */
256
function auth_login($user, $pass, $sticky = false, $silent = false) {
257
    global $USERINFO;
258
    global $conf;
259
    global $lang;
260
    /* @var DokuWiki_Auth_Plugin $auth */
261
    global $auth;
262
    /* @var Input $INPUT */
263
    global $INPUT;
264
265
    $sticky ? $sticky = true : $sticky = false; //sanity check
266
267
    if(!$auth) return false;
268
269
    if(!empty($user)) {
270
        //usual login
271
        if(!empty($pass) && $auth->checkPass($user, $pass)) {
272
            // make logininfo globally available
273
            $INPUT->server->set('REMOTE_USER', $user);
274
            $secret                 = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
275
            auth_setCookie($user, auth_encrypt($pass, $secret), $sticky);
276
            return true;
277
        } else {
278
            //invalid credentials - log off
279
            if(!$silent) {
280
                http_status(403, 'Login failed');
281
                msg($lang['badlogin'], -1);
282
            }
283
            auth_logoff();
284
            return false;
285
        }
286
    } else {
287
        // read cookie information
288
        list($user, $sticky, $pass) = auth_getCookie();
289
        if($user && $pass) {
290
            // we got a cookie - see if we can trust it
291
292
            // get session info
293
            $session = $_SESSION[DOKU_COOKIE]['auth'];
294
            if(isset($session) &&
295
                $auth->useSessionCache($user) &&
296
                ($session['time'] >= time() - $conf['auth_security_timeout']) &&
297
                ($session['user'] == $user) &&
298
                ($session['pass'] == sha1($pass)) && //still crypted
299
                ($session['buid'] == auth_browseruid())
300
            ) {
301
302
                // he has session, cookie and browser right - let him in
303
                $INPUT->server->set('REMOTE_USER', $user);
304
                $USERINFO               = $session['info']; //FIXME move all references to session
305
                return true;
306
            }
307
            // no we don't trust it yet - recheck pass but silent
308
            $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
309
            $pass   = auth_decrypt($pass, $secret);
310
            return auth_login($user, $pass, $sticky, true);
311
        }
312
    }
313
    //just to be sure
314
    auth_logoff(true);
315
    return false;
316
}
317
318
/**
319
 * Builds a pseudo UID from browser and IP data
320
 *
321
 * This is neither unique nor unfakable - still it adds some
322
 * security. Using the first part of the IP makes sure
323
 * proxy farms like AOLs are still okay.
324
 *
325
 * @author  Andreas Gohr <[email protected]>
326
 *
327
 * @return  string  a MD5 sum of various browser headers
328
 */
329
function auth_browseruid() {
330
    /* @var Input $INPUT */
331
    global $INPUT;
332
333
    $ip  = clientIP(true);
334
    $uid = '';
335
    $uid .= $INPUT->server->str('HTTP_USER_AGENT');
336
    $uid .= $INPUT->server->str('HTTP_ACCEPT_CHARSET');
337
    $uid .= substr($ip, 0, strpos($ip, '.'));
338
    $uid = strtolower($uid);
339
    return md5($uid);
340
}
341
342
/**
343
 * Creates a random key to encrypt the password in cookies
344
 *
345
 * This function tries to read the password for encrypting
346
 * cookies from $conf['metadir'].'/_htcookiesalt'
347
 * if no such file is found a random key is created and
348
 * and stored in this file.
349
 *
350
 * @author  Andreas Gohr <[email protected]>
351
 *
352
 * @param   bool $addsession if true, the sessionid is added to the salt
353
 * @param   bool $secure     if security is more important than keeping the old value
354
 * @return  string
355
 */
356
function auth_cookiesalt($addsession = false, $secure = false) {
357
    if (defined('SIMPLE_TEST')) {
358
        return 'test';
359
    }
360
    global $conf;
361
    $file = $conf['metadir'].'/_htcookiesalt';
362
    if ($secure || !file_exists($file)) {
363
        $file = $conf['metadir'].'/_htcookiesalt2';
364
    }
365
    $salt = io_readFile($file);
366
    if(empty($salt)) {
367
        $salt = bin2hex(auth_randombytes(64));
368
        io_saveFile($file, $salt);
369
    }
370
    if($addsession) {
371
        $salt .= session_id();
372
    }
373
    return $salt;
374
}
375
376
/**
377
 * Return cryptographically secure random bytes.
378
 *
379
 * @author Niklas Keller <[email protected]>
380
 *
381
 * @param int $length number of bytes
382
 * @return string cryptographically secure random bytes
383
 */
384
function auth_randombytes($length) {
385
    return random_bytes($length);
386
}
387
388
/**
389
 * Cryptographically secure random number generator.
390
 *
391
 * @author Niklas Keller <[email protected]>
392
 *
393
 * @param int $min
394
 * @param int $max
395
 * @return int
396
 */
397
function auth_random($min, $max) {
398
    return random_int($min, $max);
399
}
400
401
/**
402
 * Encrypt data using the given secret using AES
403
 *
404
 * The mode is CBC with a random initialization vector, the key is derived
405
 * using pbkdf2.
406
 *
407
 * @param string $data   The data that shall be encrypted
408
 * @param string $secret The secret/password that shall be used
409
 * @return string The ciphertext
410
 */
411
function auth_encrypt($data, $secret) {
412
    $iv     = auth_randombytes(16);
413
    $cipher = new \phpseclib\Crypt\AES();
414
    $cipher->setPassword($secret);
415
416
    /*
417
    this uses the encrypted IV as IV as suggested in
418
    http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C
419
    for unique but necessarily random IVs. The resulting ciphertext is
420
    compatible to ciphertext that was created using a "normal" IV.
421
    */
422
    return $cipher->encrypt($iv.$data);
423
}
424
425
/**
426
 * Decrypt the given AES ciphertext
427
 *
428
 * The mode is CBC, the key is derived using pbkdf2
429
 *
430
 * @param string $ciphertext The encrypted data
431
 * @param string $secret     The secret/password that shall be used
432
 * @return string The decrypted data
433
 */
434
function auth_decrypt($ciphertext, $secret) {
435
    $iv     = substr($ciphertext, 0, 16);
436
    $cipher = new \phpseclib\Crypt\AES();
437
    $cipher->setPassword($secret);
438
    $cipher->setIV($iv);
439
440
    return $cipher->decrypt(substr($ciphertext, 16));
441
}
442
443
/**
444
 * Log out the current user
445
 *
446
 * This clears all authentication data and thus log the user
447
 * off. It also clears session data.
448
 *
449
 * @author  Andreas Gohr <[email protected]>
450
 *
451
 * @param bool $keepbc - when true, the breadcrumb data is not cleared
452
 */
453
function auth_logoff($keepbc = false) {
454
    global $conf;
455
    global $USERINFO;
456
    /* @var DokuWiki_Auth_Plugin $auth */
457
    global $auth;
458
    /* @var Input $INPUT */
459
    global $INPUT;
460
461
    // make sure the session is writable (it usually is)
462
    @session_start();
463
464
    if(isset($_SESSION[DOKU_COOKIE]['auth']['user']))
465
        unset($_SESSION[DOKU_COOKIE]['auth']['user']);
466
    if(isset($_SESSION[DOKU_COOKIE]['auth']['pass']))
467
        unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
468
    if(isset($_SESSION[DOKU_COOKIE]['auth']['info']))
469
        unset($_SESSION[DOKU_COOKIE]['auth']['info']);
470
    if(!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc']))
471
        unset($_SESSION[DOKU_COOKIE]['bc']);
472
    $INPUT->server->remove('REMOTE_USER');
473
    $USERINFO = null; //FIXME
474
475
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
476
    setcookie(DOKU_COOKIE, '', time() - 600000, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
477
478
    if($auth) $auth->logOff();
479
}
480
481
/**
482
 * Check if a user is a manager
483
 *
484
 * Should usually be called without any parameters to check the current
485
 * user.
486
 *
487
 * The info is available through $INFO['ismanager'], too
488
 *
489
 * @author Andreas Gohr <[email protected]>
490
 * @see    auth_isadmin
491
 *
492
 * @param  string $user       Username
493
 * @param  array  $groups     List of groups the user is in
494
 * @param  bool   $adminonly  when true checks if user is admin
495
 * @return bool
496
 */
497
function auth_ismanager($user = null, $groups = null, $adminonly = false) {
498
    global $conf;
499
    global $USERINFO;
500
    /* @var DokuWiki_Auth_Plugin $auth */
501
    global $auth;
502
    /* @var Input $INPUT */
503
    global $INPUT;
504
505
506
    if(!$auth) return false;
507
    if(is_null($user)) {
508
        if(!$INPUT->server->has('REMOTE_USER')) {
509
            return false;
510
        } else {
511
            $user = $INPUT->server->str('REMOTE_USER');
512
        }
513
    }
514
    if(is_null($groups)) {
515
        $groups = (array) $USERINFO['grps'];
516
    }
517
518
    // check superuser match
519
    if(auth_isMember($conf['superuser'], $user, $groups)) return true;
520
    if($adminonly) return false;
521
    // check managers
522
    if(auth_isMember($conf['manager'], $user, $groups)) return true;
523
524
    return false;
525
}
526
527
/**
528
 * Check if a user is admin
529
 *
530
 * Alias to auth_ismanager with adminonly=true
531
 *
532
 * The info is available through $INFO['isadmin'], too
533
 *
534
 * @author Andreas Gohr <[email protected]>
535
 * @see auth_ismanager()
536
 *
537
 * @param  string $user       Username
538
 * @param  array  $groups     List of groups the user is in
539
 * @return bool
540
 */
541
function auth_isadmin($user = null, $groups = null) {
542
    return auth_ismanager($user, $groups, true);
543
}
544
545
/**
546
 * Match a user and his groups against a comma separated list of
547
 * users and groups to determine membership status
548
 *
549
 * Note: all input should NOT be nameencoded.
550
 *
551
 * @param string $memberlist commaseparated list of allowed users and groups
552
 * @param string $user       user to match against
553
 * @param array  $groups     groups the user is member of
554
 * @return bool       true for membership acknowledged
555
 */
556
function auth_isMember($memberlist, $user, array $groups) {
557
    /* @var DokuWiki_Auth_Plugin $auth */
558
    global $auth;
559
    if(!$auth) return false;
560
561
    // clean user and groups
562
    if(!$auth->isCaseSensitive()) {
563
        $user   = utf8_strtolower($user);
564
        $groups = array_map('utf8_strtolower', $groups);
565
    }
566
    $user   = $auth->cleanUser($user);
567
    $groups = array_map(array($auth, 'cleanGroup'), $groups);
568
569
    // extract the memberlist
570
    $members = explode(',', $memberlist);
571
    $members = array_map('trim', $members);
572
    $members = array_unique($members);
573
    $members = array_filter($members);
574
575
    // compare cleaned values
576
    foreach($members as $member) {
577
        if($member == '@ALL' ) return true;
578
        if(!$auth->isCaseSensitive()) $member = utf8_strtolower($member);
579
        if($member[0] == '@') {
580
            $member = $auth->cleanGroup(substr($member, 1));
581
            if(in_array($member, $groups)) return true;
582
        } else {
583
            $member = $auth->cleanUser($member);
584
            if($member == $user) return true;
585
        }
586
    }
587
588
    // still here? not a member!
589
    return false;
590
}
591
592
/**
593
 * Convinience function for auth_aclcheck()
594
 *
595
 * This checks the permissions for the current user
596
 *
597
 * @author  Andreas Gohr <[email protected]>
598
 *
599
 * @param  string  $id  page ID (needs to be resolved and cleaned)
600
 * @return int          permission level
601
 */
602
function auth_quickaclcheck($id) {
603
    global $conf;
604
    global $USERINFO;
605
    /* @var Input $INPUT */
606
    global $INPUT;
607
    # if no ACL is used always return upload rights
608
    if(!$conf['useacl']) return AUTH_UPLOAD;
609
    return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']);
610
}
611
612
/**
613
 * Returns the maximum rights a user has for the given ID or its namespace
614
 *
615
 * @author  Andreas Gohr <[email protected]>
616
 *
617
 * @triggers AUTH_ACL_CHECK
618
 * @param  string       $id     page ID (needs to be resolved and cleaned)
619
 * @param  string       $user   Username
620
 * @param  array|null   $groups Array of groups the user is in
621
 * @return int             permission level
622
 */
623
function auth_aclcheck($id, $user, $groups) {
624
    $data = array(
625
        'id'     => $id,
626
        'user'   => $user,
627
        'groups' => $groups
628
    );
629
630
    return trigger_event('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb');
631
}
632
633
/**
634
 * default ACL check method
635
 *
636
 * DO NOT CALL DIRECTLY, use auth_aclcheck() instead
637
 *
638
 * @author  Andreas Gohr <[email protected]>
639
 *
640
 * @param  array $data event data
641
 * @return int   permission level
642
 */
643
function auth_aclcheck_cb($data) {
644
    $id     =& $data['id'];
645
    $user   =& $data['user'];
646
    $groups =& $data['groups'];
647
648
    global $conf;
649
    global $AUTH_ACL;
650
    /* @var DokuWiki_Auth_Plugin $auth */
651
    global $auth;
652
653
    // if no ACL is used always return upload rights
654
    if(!$conf['useacl']) return AUTH_UPLOAD;
655
    if(!$auth) return AUTH_NONE;
656
657
    //make sure groups is an array
658
    if(!is_array($groups)) $groups = array();
659
660
    //if user is superuser or in superusergroup return 255 (acl_admin)
661
    if(auth_isadmin($user, $groups)) {
662
        return AUTH_ADMIN;
663
    }
664
665
    if(!$auth->isCaseSensitive()) {
666
        $user   = utf8_strtolower($user);
667
        $groups = array_map('utf8_strtolower', $groups);
668
    }
669
    $user   = auth_nameencode($auth->cleanUser($user));
670
    $groups = array_map(array($auth, 'cleanGroup'), (array) $groups);
671
672
    //prepend groups with @ and nameencode
673
    foreach($groups as &$group) {
674
        $group = '@'.auth_nameencode($group);
675
    }
676
677
    $ns   = getNS($id);
678
    $perm = -1;
679
680
    //add ALL group
681
    $groups[] = '@ALL';
682
683
    //add User
684
    if($user) $groups[] = $user;
685
686
    //check exact match first
687
    $matches = preg_grep('/^'.preg_quote($id, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
688
    if(count($matches)) {
689
        foreach($matches as $match) {
690
            $match = preg_replace('/#.*$/', '', $match); //ignore comments
691
            $acl   = preg_split('/[ \t]+/', $match);
692
            if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
693
                $acl[1] = utf8_strtolower($acl[1]);
694
            }
695
            if(!in_array($acl[1], $groups)) {
696
                continue;
697
            }
698
            if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
699
            if($acl[2] > $perm) {
700
                $perm = $acl[2];
701
            }
702
        }
703
        if($perm > -1) {
704
            //we had a match - return it
705
            return (int) $perm;
706
        }
707
    }
708
709
    //still here? do the namespace checks
710
    if($ns) {
711
        $path = $ns.':*';
712
    } else {
713
        $path = '*'; //root document
714
    }
715
716
    do {
717
        $matches = preg_grep('/^'.preg_quote($path, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
718
        if(count($matches)) {
719
            foreach($matches as $match) {
720
                $match = preg_replace('/#.*$/', '', $match); //ignore comments
721
                $acl   = preg_split('/[ \t]+/', $match);
722
                if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
723
                    $acl[1] = utf8_strtolower($acl[1]);
724
                }
725
                if(!in_array($acl[1], $groups)) {
726
                    continue;
727
                }
728
                if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
729
                if($acl[2] > $perm) {
730
                    $perm = $acl[2];
731
                }
732
            }
733
            //we had a match - return it
734
            if($perm != -1) {
735
                return (int) $perm;
736
            }
737
        }
738
        //get next higher namespace
739
        $ns = getNS($ns);
740
741
        if($path != '*') {
742
            $path = $ns.':*';
743
            if($path == ':*') $path = '*';
744
        } else {
745
            //we did this already
746
            //looks like there is something wrong with the ACL
747
            //break here
748
            msg('No ACL setup yet! Denying access to everyone.');
749
            return AUTH_NONE;
750
        }
751
    } while(1); //this should never loop endless
752
    return AUTH_NONE;
753
}
754
755
/**
756
 * Encode ASCII special chars
757
 *
758
 * Some auth backends allow special chars in their user and groupnames
759
 * The special chars are encoded with this function. Only ASCII chars
760
 * are encoded UTF-8 multibyte are left as is (different from usual
761
 * urlencoding!).
762
 *
763
 * Decoding can be done with rawurldecode
764
 *
765
 * @author Andreas Gohr <[email protected]>
766
 * @see rawurldecode()
767
 *
768
 * @param string $name
769
 * @param bool $skip_group
770
 * @return string
771
 */
772
function auth_nameencode($name, $skip_group = false) {
773
    global $cache_authname;
774
    $cache =& $cache_authname;
775
    $name  = (string) $name;
776
777
    // never encode wildcard FS#1955
778
    if($name == '%USER%') return $name;
779
    if($name == '%GROUP%') return $name;
780
781
    if(!isset($cache[$name][$skip_group])) {
782
        if($skip_group && $name{0} == '@') {
783
            $cache[$name][$skip_group] = '@'.preg_replace_callback(
784
                '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
785
                'auth_nameencode_callback', substr($name, 1)
786
            );
787
        } else {
788
            $cache[$name][$skip_group] = preg_replace_callback(
789
                '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
790
                'auth_nameencode_callback', $name
791
            );
792
        }
793
    }
794
795
    return $cache[$name][$skip_group];
796
}
797
798
/**
799
 * callback encodes the matches
800
 *
801
 * @param array $matches first complete match, next matching subpatterms
802
 * @return string
803
 */
804
function auth_nameencode_callback($matches) {
805
    return '%'.dechex(ord(substr($matches[1],-1)));
806
}
807
808
/**
809
 * Create a pronouncable password
810
 *
811
 * The $foruser variable might be used by plugins to run additional password
812
 * policy checks, but is not used by the default implementation
813
 *
814
 * @author   Andreas Gohr <[email protected]>
815
 * @link     http://www.phpbuilder.com/annotate/message.php3?id=1014451
816
 * @triggers AUTH_PASSWORD_GENERATE
817
 *
818
 * @param  string $foruser username for which the password is generated
819
 * @return string  pronouncable password
820
 */
821
function auth_pwgen($foruser = '') {
822
    $data = array(
823
        'password' => '',
824
        'foruser'  => $foruser
825
    );
826
827
    $evt = new Doku_Event('AUTH_PASSWORD_GENERATE', $data);
828
    if($evt->advise_before(true)) {
829
        $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
830
        $v = 'aeiou'; //vowels
831
        $a = $c.$v; //both
832
        $s = '!$%&?+*~#-_:.;,'; // specials
833
834
        //use thre syllables...
835
        for($i = 0; $i < 3; $i++) {
836
            $data['password'] .= $c[auth_random(0, strlen($c) - 1)];
837
            $data['password'] .= $v[auth_random(0, strlen($v) - 1)];
838
            $data['password'] .= $a[auth_random(0, strlen($a) - 1)];
839
        }
840
        //... and add a nice number and special
841
        $data['password'] .= auth_random(10, 99).$s[auth_random(0, strlen($s) - 1)];
842
    }
843
    $evt->advise_after();
844
845
    return $data['password'];
846
}
847
848
/**
849
 * Sends a password to the given user
850
 *
851
 * @author  Andreas Gohr <[email protected]>
852
 *
853
 * @param string $user Login name of the user
854
 * @param string $password The new password in clear text
855
 * @return bool  true on success
856
 */
857
function auth_sendPassword($user, $password) {
858
    global $lang;
859
    /* @var DokuWiki_Auth_Plugin $auth */
860
    global $auth;
861
    if(!$auth) return false;
862
863
    $user     = $auth->cleanUser($user);
864
    $userinfo = $auth->getUserData($user, $requireGroups = false);
865
866
    if(!$userinfo['mail']) return false;
867
868
    $text = rawLocale('password');
869
    $trep = array(
870
        'FULLNAME' => $userinfo['name'],
871
        'LOGIN'    => $user,
872
        'PASSWORD' => $password
873
    );
874
875
    $mail = new Mailer();
876
    $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>');
877
    $mail->subject($lang['regpwmail']);
878
    $mail->setBody($text, $trep);
879
    return $mail->send();
880
}
881
882
/**
883
 * Register a new user
884
 *
885
 * This registers a new user - Data is read directly from $_POST
886
 *
887
 * @author  Andreas Gohr <[email protected]>
888
 *
889
 * @return bool  true on success, false on any error
890
 */
891
function register() {
892
    global $lang;
893
    global $conf;
894
    /* @var DokuWiki_Auth_Plugin $auth */
895
    global $auth;
896
    global $INPUT;
897
898
    if(!$INPUT->post->bool('save')) return false;
899
    if(!actionOK('register')) return false;
900
901
    // gather input
902
    $login    = trim($auth->cleanUser($INPUT->post->str('login')));
903
    $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname')));
904
    $email    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email')));
905
    $pass     = $INPUT->post->str('pass');
906
    $passchk  = $INPUT->post->str('passchk');
907
908
    if(empty($login) || empty($fullname) || empty($email)) {
909
        msg($lang['regmissing'], -1);
910
        return false;
911
    }
912
913
    if($conf['autopasswd']) {
914
        $pass = auth_pwgen($login); // automatically generate password
915
    } elseif(empty($pass) || empty($passchk)) {
916
        msg($lang['regmissing'], -1); // complain about missing passwords
917
        return false;
918
    } elseif($pass != $passchk) {
919
        msg($lang['regbadpass'], -1); // complain about misspelled passwords
920
        return false;
921
    }
922
923
    //check mail
924
    if(!mail_isvalid($email)) {
925
        msg($lang['regbadmail'], -1);
926
        return false;
927
    }
928
929
    //okay try to create the user
930
    if(!$auth->triggerUserMod('create', array($login, $pass, $fullname, $email))) {
931
        msg($lang['regfail'], -1);
932
        return false;
933
    }
934
935
    // send notification about the new user
936
    $subscription = new Subscription();
937
    $subscription->send_register($login, $fullname, $email);
938
939
    // are we done?
940
    if(!$conf['autopasswd']) {
941
        msg($lang['regsuccess2'], 1);
942
        return true;
943
    }
944
945
    // autogenerated password? then send password to user
946
    if(auth_sendPassword($login, $pass)) {
947
        msg($lang['regsuccess'], 1);
948
        return true;
949
    } else {
950
        msg($lang['regmailfail'], -1);
951
        return false;
952
    }
953
}
954
955
/**
956
 * Update user profile
957
 *
958
 * @author    Christopher Smith <[email protected]>
959
 */
960
function updateprofile() {
961
    global $conf;
962
    global $lang;
963
    /* @var DokuWiki_Auth_Plugin $auth */
964
    global $auth;
965
    /* @var Input $INPUT */
966
    global $INPUT;
967
968
    if(!$INPUT->post->bool('save')) return false;
969
    if(!checkSecurityToken()) return false;
970
971
    if(!actionOK('profile')) {
972
        msg($lang['profna'], -1);
973
        return false;
974
    }
975
976
    $changes         = array();
977
    $changes['pass'] = $INPUT->post->str('newpass');
978
    $changes['name'] = $INPUT->post->str('fullname');
979
    $changes['mail'] = $INPUT->post->str('email');
980
981
    // check misspelled passwords
982
    if($changes['pass'] != $INPUT->post->str('passchk')) {
983
        msg($lang['regbadpass'], -1);
984
        return false;
985
    }
986
987
    // clean fullname and email
988
    $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
989
    $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
990
991
    // no empty name and email (except the backend doesn't support them)
992
    if((empty($changes['name']) && $auth->canDo('modName')) ||
993
        (empty($changes['mail']) && $auth->canDo('modMail'))
994
    ) {
995
        msg($lang['profnoempty'], -1);
996
        return false;
997
    }
998
    if(!mail_isvalid($changes['mail']) && $auth->canDo('modMail')) {
999
        msg($lang['regbadmail'], -1);
1000
        return false;
1001
    }
1002
1003
    $changes = array_filter($changes);
1004
1005
    // check for unavailable capabilities
1006
    if(!$auth->canDo('modName')) unset($changes['name']);
1007
    if(!$auth->canDo('modMail')) unset($changes['mail']);
1008
    if(!$auth->canDo('modPass')) unset($changes['pass']);
1009
1010
    // anything to do?
1011
    if(!count($changes)) {
1012
        msg($lang['profnochange'], -1);
1013
        return false;
1014
    }
1015
1016
    if($conf['profileconfirm']) {
1017
        if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1018
            msg($lang['badpassconfirm'], -1);
1019
            return false;
1020
        }
1021
    }
1022
1023
    if(!$auth->triggerUserMod('modify', array($INPUT->server->str('REMOTE_USER'), &$changes))) {
1024
        msg($lang['proffail'], -1);
1025
        return false;
1026
    }
1027
1028
    if($changes['pass']) {
1029
        // update cookie and session with the changed data
1030
        list( /*user*/, $sticky, /*pass*/) = auth_getCookie();
1031
        $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true));
1032
        auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky);
1033
    } else {
1034
        // make sure the session is writable
1035
        @session_start();
1036
        // invalidate session cache
1037
        $_SESSION[DOKU_COOKIE]['auth']['time'] = 0;
1038
        session_write_close();
1039
    }
1040
1041
    return true;
1042
}
1043
1044
/**
1045
 * Delete the current logged-in user
1046
 *
1047
 * @return bool true on success, false on any error
1048
 */
1049
function auth_deleteprofile(){
1050
    global $conf;
1051
    global $lang;
1052
    /* @var DokuWiki_Auth_Plugin $auth */
1053
    global $auth;
1054
    /* @var Input $INPUT */
1055
    global $INPUT;
1056
1057
    if(!$INPUT->post->bool('delete')) return false;
1058
    if(!checkSecurityToken()) return false;
1059
1060
    // action prevented or auth module disallows
1061
    if(!actionOK('profile_delete') || !$auth->canDo('delUser')) {
1062
        msg($lang['profnodelete'], -1);
1063
        return false;
1064
    }
1065
1066
    if(!$INPUT->post->bool('confirm_delete')){
1067
        msg($lang['profconfdeletemissing'], -1);
1068
        return false;
1069
    }
1070
1071
    if($conf['profileconfirm']) {
1072
        if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1073
            msg($lang['badpassconfirm'], -1);
1074
            return false;
1075
        }
1076
    }
1077
1078
    $deleted = array();
1079
    $deleted[] = $INPUT->server->str('REMOTE_USER');
1080
    if($auth->triggerUserMod('delete', array($deleted))) {
1081
        // force and immediate logout including removing the sticky cookie
1082
        auth_logoff();
1083
        return true;
1084
    }
1085
1086
    return false;
1087
}
1088
1089
/**
1090
 * Send a  new password
1091
 *
1092
 * This function handles both phases of the password reset:
1093
 *
1094
 *   - handling the first request of password reset
1095
 *   - validating the password reset auth token
1096
 *
1097
 * @author Benoit Chesneau <[email protected]>
1098
 * @author Chris Smith <[email protected]>
1099
 * @author Andreas Gohr <[email protected]>
1100
 *
1101
 * @return bool true on success, false on any error
1102
 */
1103
function act_resendpwd() {
1104
    global $lang;
1105
    global $conf;
1106
    /* @var DokuWiki_Auth_Plugin $auth */
1107
    global $auth;
1108
    /* @var Input $INPUT */
1109
    global $INPUT;
1110
1111
    if(!actionOK('resendpwd')) {
1112
        msg($lang['resendna'], -1);
1113
        return false;
1114
    }
1115
1116
    $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
1117
1118
    if($token) {
1119
        // we're in token phase - get user info from token
1120
1121
        $tfile = $conf['cachedir'].'/'.$token{0}.'/'.$token.'.pwauth';
1122
        if(!file_exists($tfile)) {
1123
            msg($lang['resendpwdbadauth'], -1);
1124
            $INPUT->remove('pwauth');
1125
            return false;
1126
        }
1127
        // token is only valid for 3 days
1128
        if((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
1129
            msg($lang['resendpwdbadauth'], -1);
1130
            $INPUT->remove('pwauth');
1131
            @unlink($tfile);
1132
            return false;
1133
        }
1134
1135
        $user     = io_readfile($tfile);
1136
        $userinfo = $auth->getUserData($user, $requireGroups = false);
1137
        if(!$userinfo['mail']) {
1138
            msg($lang['resendpwdnouser'], -1);
1139
            return false;
1140
        }
1141
1142
        if(!$conf['autopasswd']) { // we let the user choose a password
1143
            $pass = $INPUT->str('pass');
1144
1145
            // password given correctly?
1146
            if(!$pass) return false;
1147
            if($pass != $INPUT->str('passchk')) {
1148
                msg($lang['regbadpass'], -1);
1149
                return false;
1150
            }
1151
1152
            // change it
1153
            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
1154
                msg($lang['proffail'], -1);
1155
                return false;
1156
            }
1157
1158
        } else { // autogenerate the password and send by mail
1159
1160
            $pass = auth_pwgen($user);
1161
            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
1162
                msg($lang['proffail'], -1);
1163
                return false;
1164
            }
1165
1166
            if(auth_sendPassword($user, $pass)) {
1167
                msg($lang['resendpwdsuccess'], 1);
1168
            } else {
1169
                msg($lang['regmailfail'], -1);
1170
            }
1171
        }
1172
1173
        @unlink($tfile);
1174
        return true;
1175
1176
    } else {
1177
        // we're in request phase
1178
1179
        if(!$INPUT->post->bool('save')) return false;
1180
1181
        if(!$INPUT->post->str('login')) {
1182
            msg($lang['resendpwdmissing'], -1);
1183
            return false;
1184
        } else {
1185
            $user = trim($auth->cleanUser($INPUT->post->str('login')));
1186
        }
1187
1188
        $userinfo = $auth->getUserData($user, $requireGroups = false);
1189
        if(!$userinfo['mail']) {
1190
            msg($lang['resendpwdnouser'], -1);
1191
            return false;
1192
        }
1193
1194
        // generate auth token
1195
        $token = md5(auth_randombytes(16)); // random secret
1196
        $tfile = $conf['cachedir'].'/'.$token{0}.'/'.$token.'.pwauth';
1197
        $url   = wl('', array('do'=> 'resendpwd', 'pwauth'=> $token), true, '&');
1198
1199
        io_saveFile($tfile, $user);
1200
1201
        $text = rawLocale('pwconfirm');
1202
        $trep = array(
1203
            'FULLNAME' => $userinfo['name'],
1204
            'LOGIN'    => $user,
1205
            'CONFIRM'  => $url
1206
        );
1207
1208
        $mail = new Mailer();
1209
        $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>');
1210
        $mail->subject($lang['regpwmail']);
1211
        $mail->setBody($text, $trep);
1212
        if($mail->send()) {
1213
            msg($lang['resendpwdconfirm'], 1);
1214
        } else {
1215
            msg($lang['regmailfail'], -1);
1216
        }
1217
        return true;
1218
    }
1219
    // never reached
1220
}
1221
1222
/**
1223
 * Encrypts a password using the given method and salt
1224
 *
1225
 * If the selected method needs a salt and none was given, a random one
1226
 * is chosen.
1227
 *
1228
 * @author  Andreas Gohr <[email protected]>
1229
 *
1230
 * @param string $clear The clear text password
1231
 * @param string $method The hashing method
1232
 * @param string $salt A salt, null for random
1233
 * @return  string  The crypted password
1234
 */
1235
function auth_cryptPassword($clear, $method = '', $salt = null) {
1236
    global $conf;
1237
    if(empty($method)) $method = $conf['passcrypt'];
1238
1239
    $pass = new PassHash();
1240
    $call = 'hash_'.$method;
1241
1242
    if(!method_exists($pass, $call)) {
1243
        msg("Unsupported crypt method $method", -1);
1244
        return false;
1245
    }
1246
1247
    return $pass->$call($clear, $salt);
1248
}
1249
1250
/**
1251
 * Verifies a cleartext password against a crypted hash
1252
 *
1253
 * @author Andreas Gohr <[email protected]>
1254
 *
1255
 * @param  string $clear The clear text password
1256
 * @param  string $crypt The hash to compare with
1257
 * @return bool true if both match
1258
 */
1259
function auth_verifyPassword($clear, $crypt) {
1260
    $pass = new PassHash();
1261
    return $pass->verify_hash($clear, $crypt);
1262
}
1263
1264
/**
1265
 * Set the authentication cookie and add user identification data to the session
1266
 *
1267
 * @param string  $user       username
1268
 * @param string  $pass       encrypted password
1269
 * @param bool    $sticky     whether or not the cookie will last beyond the session
1270
 * @return bool
1271
 */
1272
function auth_setCookie($user, $pass, $sticky) {
1273
    global $conf;
1274
    /* @var DokuWiki_Auth_Plugin $auth */
1275
    global $auth;
1276
    global $USERINFO;
1277
1278
    if(!$auth) return false;
1279
    $USERINFO = $auth->getUserData($user);
1280
1281
    // set cookie
1282
    $cookie    = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode($pass);
1283
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1284
    $time      = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
1285
    setcookie(DOKU_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
1286
1287
    // set session
1288
    $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
1289
    $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
1290
    $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
1291
    $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
1292
    $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
1293
1294
    return true;
1295
}
1296
1297
/**
1298
 * Returns the user, (encrypted) password and sticky bit from cookie
1299
 *
1300
 * @returns array
1301
 */
1302
function auth_getCookie() {
1303
    if(!isset($_COOKIE[DOKU_COOKIE])) {
1304
        return array(null, null, null);
1305
    }
1306
    list($user, $sticky, $pass) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
1307
    $sticky = (bool) $sticky;
1308
    $pass   = base64_decode($pass);
1309
    $user   = base64_decode($user);
1310
    return array($user, $sticky, $pass);
1311
}
1312
1313
//Setup VIM: ex: et ts=2 :
1314