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