Failed Conditions
Push — psr2 ( d8b492...c3cc6e )
by Andreas
03:04
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
// some ACL level defines
13
use dokuwiki\PassHash;
0 ignored issues
show
This use statement conflicts with another class in this namespace, PassHash.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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