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