Failed Conditions
Pull Request — psr2 (#2426)
by Andreas
09:22 queued 06:07
created

auth.php ➔ auth_login()   C

Complexity

Conditions 15
Paths 14

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
nc 14
nop 4
dl 0
loc 61
rs 5.9166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Extension\AuthPlugin;
14
use dokuwiki\Extension\PluginController;
15
use dokuwiki\Extension\Event;
16
17
define('AUTH_NONE', 0);
18
define('AUTH_READ', 1);
19
define('AUTH_EDIT', 2);
20
define('AUTH_CREATE', 4);
21
define('AUTH_UPLOAD', 8);
22
define('AUTH_DELETE', 16);
23
define('AUTH_ADMIN', 255);
24
25
/**
26
 * Initialize the auth system.
27
 *
28
 * This function is automatically called at the end of init.php
29
 *
30
 * This used to be the main() of the auth.php
31
 *
32
 * @todo backend loading maybe should be handled by the class autoloader
33
 * @todo maybe split into multiple functions at the XXX marked positions
34
 * @triggers AUTH_LOGIN_CHECK
35
 * @return bool
36
 */
37
function auth_setup() {
38
    global $conf;
39
    /* @var AuthPlugin $auth */
40
    global $auth;
41
    /* @var Input $INPUT */
42
    global $INPUT;
43
    global $AUTH_ACL;
44
    global $lang;
45
    /* @var PluginController $plugin_controller */
46
    global $plugin_controller;
47
    $AUTH_ACL = array();
48
49
    if(!$conf['useacl']) return false;
50
51
    // try to load auth backend from plugins
52
    foreach ($plugin_controller->getList('auth') as $plugin) {
53
        if ($conf['authtype'] === $plugin) {
54
            $auth = $plugin_controller->load('auth', $plugin);
55
            break;
56
        }
57
    }
58
59
    if(!isset($auth) || !$auth){
60
        msg($lang['authtempfail'], -1);
61
        return false;
62
    }
63
64
    if ($auth->success == false) {
0 ignored issues
show
Bug introduced by
Accessing success on the interface dokuwiki\Extension\PluginInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
65
        // degrade to unauthenticated user
66
        unset($auth);
67
        auth_logoff();
68
        msg($lang['authtempfail'], -1);
69
        return false;
70
    }
71
72
    // do the login either by cookie or provided credentials XXX
73
    $INPUT->set('http_credentials', false);
74
    if(!$conf['rememberme']) $INPUT->set('r', false);
75
76
    // handle renamed HTTP_AUTHORIZATION variable (can happen when a fix like
77
    // the one presented at
78
    // http://www.besthostratings.com/articles/http-auth-php-cgi.html is used
79
    // for enabling HTTP authentication with CGI/SuExec)
80
    if(isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))
81
        $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
82
    // streamline HTTP auth credentials (IIS/rewrite -> mod_php)
83
    if(isset($_SERVER['HTTP_AUTHORIZATION'])) {
84
        list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
85
            explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
86
    }
87
88
    // if no credentials were given try to use HTTP auth (for SSO)
89
    if(!$INPUT->str('u') && empty($_COOKIE[DOKU_COOKIE]) && !empty($_SERVER['PHP_AUTH_USER'])) {
90
        $INPUT->set('u', $_SERVER['PHP_AUTH_USER']);
91
        $INPUT->set('p', $_SERVER['PHP_AUTH_PW']);
92
        $INPUT->set('http_credentials', true);
93
    }
94
95
    // apply cleaning (auth specific user names, remove control chars)
96
    if (true === $auth->success) {
0 ignored issues
show
Bug introduced by
Accessing success on the interface dokuwiki\Extension\PluginInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
97
        $INPUT->set('u', $auth->cleanUser(stripctl($INPUT->str('u'))));
98
        $INPUT->set('p', stripctl($INPUT->str('p')));
99
    }
100
101
    if(!is_null($auth) && $auth->canDo('external')) {
102
        // external trust mechanism in place
103
        $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r'));
104
    } else {
105
        $evdata = array(
106
            'user'     => $INPUT->str('u'),
107
            'password' => $INPUT->str('p'),
108
            'sticky'   => $INPUT->bool('r'),
109
            'silent'   => $INPUT->bool('http_credentials')
110
        );
111
        Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
112
    }
113
114
    //load ACL into a global array XXX
115
    $AUTH_ACL = auth_loadACL();
116
117
    return true;
118
}
119
120
/**
121
 * Loads the ACL setup and handle user wildcards
122
 *
123
 * @author Andreas Gohr <[email protected]>
124
 *
125
 * @return array
126
 */
127
function auth_loadACL() {
128
    global $config_cascade;
129
    global $USERINFO;
130
    /* @var Input $INPUT */
131
    global $INPUT;
132
133
    if(!is_readable($config_cascade['acl']['default'])) return array();
134
135
    $acl = file($config_cascade['acl']['default']);
136
137
    $out = array();
138
    foreach($acl as $line) {
139
        $line = trim($line);
140
        if(empty($line) || ($line{0} == '#')) continue; // skip blank lines & comments
141
        list($id,$rest) = preg_split('/[ \t]+/',$line,2);
142
143
        // substitute user wildcard first (its 1:1)
144
        if(strstr($line, '%USER%')){
145
            // if user is not logged in, this ACL line is meaningless - skip it
146
            if (!$INPUT->server->has('REMOTE_USER')) continue;
147
148
            $id   = str_replace('%USER%',cleanID($INPUT->server->str('REMOTE_USER')),$id);
149
            $rest = str_replace('%USER%',auth_nameencode($INPUT->server->str('REMOTE_USER')),$rest);
150
        }
151
152
        // substitute group wildcard (its 1:m)
153
        if(strstr($line, '%GROUP%')){
154
            // if user is not logged in, grps is empty, no output will be added (i.e. skipped)
155
            foreach((array) $USERINFO['grps'] as $grp){
156
                $nid   = str_replace('%GROUP%',cleanID($grp),$id);
157
                $nrest = str_replace('%GROUP%','@'.auth_nameencode($grp),$rest);
158
                $out[] = "$nid\t$nrest";
159
            }
160
        } else {
161
            $out[] = "$id\t$rest";
162
        }
163
    }
164
165
    return $out;
166
}
167
168
/**
169
 * Event hook callback for AUTH_LOGIN_CHECK
170
 *
171
 * @param array $evdata
172
 * @return bool
173
 */
174
function auth_login_wrapper($evdata) {
175
    return auth_login(
176
        $evdata['user'],
177
        $evdata['password'],
178
        $evdata['sticky'],
179
        $evdata['silent']
180
    );
181
}
182
183
/**
184
 * This tries to login the user based on the sent auth credentials
185
 *
186
 * The authentication works like this: if a username was given
187
 * a new login is assumed and user/password are checked. If they
188
 * are correct the password is encrypted with blowfish and stored
189
 * together with the username in a cookie - the same info is stored
190
 * in the session, too. Additonally a browserID is stored in the
191
 * session.
192
 *
193
 * If no username was given the cookie is checked: if the username,
194
 * crypted password and browserID match between session and cookie
195
 * no further testing is done and the user is accepted
196
 *
197
 * If a cookie was found but no session info was availabe the
198
 * blowfish encrypted password from the cookie is decrypted and
199
 * together with username rechecked by calling this function again.
200
 *
201
 * On a successful login $_SERVER[REMOTE_USER] and $USERINFO
202
 * are set.
203
 *
204
 * @author  Andreas Gohr <[email protected]>
205
 *
206
 * @param   string  $user    Username
207
 * @param   string  $pass    Cleartext Password
208
 * @param   bool    $sticky  Cookie should not expire
209
 * @param   bool    $silent  Don't show error on bad auth
210
 * @return  bool             true on successful auth
211
 */
212
function auth_login($user, $pass, $sticky = false, $silent = false) {
213
    global $USERINFO;
214
    global $conf;
215
    global $lang;
216
    /* @var AuthPlugin $auth */
217
    global $auth;
218
    /* @var Input $INPUT */
219
    global $INPUT;
220
221
    $sticky ? $sticky = true : $sticky = false; //sanity check
222
223
    if(!$auth) return false;
224
225
    if(!empty($user)) {
226
        //usual login
227
        if(!empty($pass) && $auth->checkPass($user, $pass)) {
228
            // make logininfo globally available
229
            $INPUT->server->set('REMOTE_USER', $user);
230
            $secret                 = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
231
            auth_setCookie($user, auth_encrypt($pass, $secret), $sticky);
232
            return true;
233
        } else {
234
            //invalid credentials - log off
235
            if(!$silent) {
236
                http_status(403, 'Login failed');
237
                msg($lang['badlogin'], -1);
238
            }
239
            auth_logoff();
240
            return false;
241
        }
242
    } else {
243
        // read cookie information
244
        list($user, $sticky, $pass) = auth_getCookie();
245
        if($user && $pass) {
246
            // we got a cookie - see if we can trust it
247
248
            // get session info
249
            $session = $_SESSION[DOKU_COOKIE]['auth'];
250
            if(isset($session) &&
251
                $auth->useSessionCache($user) &&
252
                ($session['time'] >= time() - $conf['auth_security_timeout']) &&
253
                ($session['user'] == $user) &&
254
                ($session['pass'] == sha1($pass)) && //still crypted
255
                ($session['buid'] == auth_browseruid())
256
            ) {
257
258
                // he has session, cookie and browser right - let him in
259
                $INPUT->server->set('REMOTE_USER', $user);
260
                $USERINFO               = $session['info']; //FIXME move all references to session
261
                return true;
262
            }
263
            // no we don't trust it yet - recheck pass but silent
264
            $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
265
            $pass   = auth_decrypt($pass, $secret);
266
            return auth_login($user, $pass, $sticky, true);
267
        }
268
    }
269
    //just to be sure
270
    auth_logoff(true);
271
    return false;
272
}
273
274
/**
275
 * Builds a pseudo UID from browser and IP data
276
 *
277
 * This is neither unique nor unfakable - still it adds some
278
 * security. Using the first part of the IP makes sure
279
 * proxy farms like AOLs are still okay.
280
 *
281
 * @author  Andreas Gohr <[email protected]>
282
 *
283
 * @return  string  a MD5 sum of various browser headers
284
 */
285
function auth_browseruid() {
286
    /* @var Input $INPUT */
287
    global $INPUT;
288
289
    $ip  = clientIP(true);
290
    $uid = '';
291
    $uid .= $INPUT->server->str('HTTP_USER_AGENT');
292
    $uid .= $INPUT->server->str('HTTP_ACCEPT_CHARSET');
293
    $uid .= substr($ip, 0, strpos($ip, '.'));
294
    $uid = strtolower($uid);
295
    return md5($uid);
296
}
297
298
/**
299
 * Creates a random key to encrypt the password in cookies
300
 *
301
 * This function tries to read the password for encrypting
302
 * cookies from $conf['metadir'].'/_htcookiesalt'
303
 * if no such file is found a random key is created and
304
 * and stored in this file.
305
 *
306
 * @author  Andreas Gohr <[email protected]>
307
 *
308
 * @param   bool $addsession if true, the sessionid is added to the salt
309
 * @param   bool $secure     if security is more important than keeping the old value
310
 * @return  string
311
 */
312
function auth_cookiesalt($addsession = false, $secure = false) {
313
    if (defined('SIMPLE_TEST')) {
314
        return 'test';
315
    }
316
    global $conf;
317
    $file = $conf['metadir'].'/_htcookiesalt';
318
    if ($secure || !file_exists($file)) {
319
        $file = $conf['metadir'].'/_htcookiesalt2';
320
    }
321
    $salt = io_readFile($file);
322
    if(empty($salt)) {
323
        $salt = bin2hex(auth_randombytes(64));
324
        io_saveFile($file, $salt);
325
    }
326
    if($addsession) {
327
        $salt .= session_id();
328
    }
329
    return $salt;
330
}
331
332
/**
333
 * Return cryptographically secure random bytes.
334
 *
335
 * @author Niklas Keller <[email protected]>
336
 *
337
 * @param int $length number of bytes
338
 * @return string cryptographically secure random bytes
339
 */
340
function auth_randombytes($length) {
341
    return random_bytes($length);
342
}
343
344
/**
345
 * Cryptographically secure random number generator.
346
 *
347
 * @author Niklas Keller <[email protected]>
348
 *
349
 * @param int $min
350
 * @param int $max
351
 * @return int
352
 */
353
function auth_random($min, $max) {
354
    return random_int($min, $max);
355
}
356
357
/**
358
 * Encrypt data using the given secret using AES
359
 *
360
 * The mode is CBC with a random initialization vector, the key is derived
361
 * using pbkdf2.
362
 *
363
 * @param string $data   The data that shall be encrypted
364
 * @param string $secret The secret/password that shall be used
365
 * @return string The ciphertext
366
 */
367
function auth_encrypt($data, $secret) {
368
    $iv     = auth_randombytes(16);
369
    $cipher = new \phpseclib\Crypt\AES();
370
    $cipher->setPassword($secret);
371
372
    /*
373
    this uses the encrypted IV as IV as suggested in
374
    http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C
375
    for unique but necessarily random IVs. The resulting ciphertext is
376
    compatible to ciphertext that was created using a "normal" IV.
377
    */
378
    return $cipher->encrypt($iv.$data);
379
}
380
381
/**
382
 * Decrypt the given AES ciphertext
383
 *
384
 * The mode is CBC, the key is derived using pbkdf2
385
 *
386
 * @param string $ciphertext The encrypted data
387
 * @param string $secret     The secret/password that shall be used
388
 * @return string The decrypted data
389
 */
390
function auth_decrypt($ciphertext, $secret) {
391
    $iv     = substr($ciphertext, 0, 16);
392
    $cipher = new \phpseclib\Crypt\AES();
393
    $cipher->setPassword($secret);
394
    $cipher->setIV($iv);
395
396
    return $cipher->decrypt(substr($ciphertext, 16));
397
}
398
399
/**
400
 * Log out the current user
401
 *
402
 * This clears all authentication data and thus log the user
403
 * off. It also clears session data.
404
 *
405
 * @author  Andreas Gohr <[email protected]>
406
 *
407
 * @param bool $keepbc - when true, the breadcrumb data is not cleared
408
 */
409
function auth_logoff($keepbc = false) {
410
    global $conf;
411
    global $USERINFO;
412
    /* @var AuthPlugin $auth */
413
    global $auth;
414
    /* @var Input $INPUT */
415
    global $INPUT;
416
417
    // make sure the session is writable (it usually is)
418
    @session_start();
419
420
    if(isset($_SESSION[DOKU_COOKIE]['auth']['user']))
421
        unset($_SESSION[DOKU_COOKIE]['auth']['user']);
422
    if(isset($_SESSION[DOKU_COOKIE]['auth']['pass']))
423
        unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
424
    if(isset($_SESSION[DOKU_COOKIE]['auth']['info']))
425
        unset($_SESSION[DOKU_COOKIE]['auth']['info']);
426
    if(!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc']))
427
        unset($_SESSION[DOKU_COOKIE]['bc']);
428
    $INPUT->server->remove('REMOTE_USER');
429
    $USERINFO = null; //FIXME
430
431
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
432
    setcookie(DOKU_COOKIE, '', time() - 600000, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
433
434
    if($auth) $auth->logOff();
435
}
436
437
/**
438
 * Check if a user is a manager
439
 *
440
 * Should usually be called without any parameters to check the current
441
 * user.
442
 *
443
 * The info is available through $INFO['ismanager'], too
444
 *
445
 * @author Andreas Gohr <[email protected]>
446
 * @see    auth_isadmin
447
 *
448
 * @param  string $user       Username
449
 * @param  array  $groups     List of groups the user is in
450
 * @param  bool   $adminonly  when true checks if user is admin
451
 * @return bool
452
 */
453
function auth_ismanager($user = null, $groups = null, $adminonly = false) {
454
    global $conf;
455
    global $USERINFO;
456
    /* @var AuthPlugin $auth */
457
    global $auth;
458
    /* @var Input $INPUT */
459
    global $INPUT;
460
461
462
    if(!$auth) return false;
463
    if(is_null($user)) {
464
        if(!$INPUT->server->has('REMOTE_USER')) {
465
            return false;
466
        } else {
467
            $user = $INPUT->server->str('REMOTE_USER');
468
        }
469
    }
470
    if(is_null($groups)) {
471
        $groups = (array) $USERINFO['grps'];
472
    }
473
474
    // check superuser match
475
    if(auth_isMember($conf['superuser'], $user, $groups)) return true;
476
    if($adminonly) return false;
477
    // check managers
478
    if(auth_isMember($conf['manager'], $user, $groups)) return true;
479
480
    return false;
481
}
482
483
/**
484
 * Check if a user is admin
485
 *
486
 * Alias to auth_ismanager with adminonly=true
487
 *
488
 * The info is available through $INFO['isadmin'], too
489
 *
490
 * @author Andreas Gohr <[email protected]>
491
 * @see auth_ismanager()
492
 *
493
 * @param  string $user       Username
494
 * @param  array  $groups     List of groups the user is in
495
 * @return bool
496
 */
497
function auth_isadmin($user = null, $groups = null) {
498
    return auth_ismanager($user, $groups, true);
499
}
500
501
/**
502
 * Match a user and his groups against a comma separated list of
503
 * users and groups to determine membership status
504
 *
505
 * Note: all input should NOT be nameencoded.
506
 *
507
 * @param string $memberlist commaseparated list of allowed users and groups
508
 * @param string $user       user to match against
509
 * @param array  $groups     groups the user is member of
510
 * @return bool       true for membership acknowledged
511
 */
512
function auth_isMember($memberlist, $user, array $groups) {
513
    /* @var AuthPlugin $auth */
514
    global $auth;
515
    if(!$auth) return false;
516
517
    // clean user and groups
518
    if(!$auth->isCaseSensitive()) {
519
        $user   = utf8_strtolower($user);
520
        $groups = array_map('utf8_strtolower', $groups);
521
    }
522
    $user   = $auth->cleanUser($user);
523
    $groups = array_map(array($auth, 'cleanGroup'), $groups);
524
525
    // extract the memberlist
526
    $members = explode(',', $memberlist);
527
    $members = array_map('trim', $members);
528
    $members = array_unique($members);
529
    $members = array_filter($members);
530
531
    // compare cleaned values
532
    foreach($members as $member) {
533
        if($member == '@ALL' ) return true;
534
        if(!$auth->isCaseSensitive()) $member = utf8_strtolower($member);
535
        if($member[0] == '@') {
536
            $member = $auth->cleanGroup(substr($member, 1));
537
            if(in_array($member, $groups)) return true;
538
        } else {
539
            $member = $auth->cleanUser($member);
540
            if($member == $user) return true;
541
        }
542
    }
543
544
    // still here? not a member!
545
    return false;
546
}
547
548
/**
549
 * Convinience function for auth_aclcheck()
550
 *
551
 * This checks the permissions for the current user
552
 *
553
 * @author  Andreas Gohr <[email protected]>
554
 *
555
 * @param  string  $id  page ID (needs to be resolved and cleaned)
556
 * @return int          permission level
557
 */
558
function auth_quickaclcheck($id) {
559
    global $conf;
560
    global $USERINFO;
561
    /* @var Input $INPUT */
562
    global $INPUT;
563
    # if no ACL is used always return upload rights
564
    if(!$conf['useacl']) return AUTH_UPLOAD;
565
    return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), $USERINFO['grps']);
566
}
567
568
/**
569
 * Returns the maximum rights a user has for the given ID or its namespace
570
 *
571
 * @author  Andreas Gohr <[email protected]>
572
 *
573
 * @triggers AUTH_ACL_CHECK
574
 * @param  string       $id     page ID (needs to be resolved and cleaned)
575
 * @param  string       $user   Username
576
 * @param  array|null   $groups Array of groups the user is in
577
 * @return int             permission level
578
 */
579
function auth_aclcheck($id, $user, $groups) {
580
    $data = array(
581
        'id'     => $id,
582
        'user'   => $user,
583
        'groups' => $groups
584
    );
585
586
    return Event::createAndTrigger('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb');
587
}
588
589
/**
590
 * default ACL check method
591
 *
592
 * DO NOT CALL DIRECTLY, use auth_aclcheck() instead
593
 *
594
 * @author  Andreas Gohr <[email protected]>
595
 *
596
 * @param  array $data event data
597
 * @return int   permission level
598
 */
599
function auth_aclcheck_cb($data) {
600
    $id     =& $data['id'];
601
    $user   =& $data['user'];
602
    $groups =& $data['groups'];
603
604
    global $conf;
605
    global $AUTH_ACL;
606
    /* @var AuthPlugin $auth */
607
    global $auth;
608
609
    // if no ACL is used always return upload rights
610
    if(!$conf['useacl']) return AUTH_UPLOAD;
611
    if(!$auth) return AUTH_NONE;
612
613
    //make sure groups is an array
614
    if(!is_array($groups)) $groups = array();
615
616
    //if user is superuser or in superusergroup return 255 (acl_admin)
617
    if(auth_isadmin($user, $groups)) {
618
        return AUTH_ADMIN;
619
    }
620
621
    if(!$auth->isCaseSensitive()) {
622
        $user   = utf8_strtolower($user);
623
        $groups = array_map('utf8_strtolower', $groups);
624
    }
625
    $user   = auth_nameencode($auth->cleanUser($user));
626
    $groups = array_map(array($auth, 'cleanGroup'), (array) $groups);
627
628
    //prepend groups with @ and nameencode
629
    foreach($groups as &$group) {
630
        $group = '@'.auth_nameencode($group);
631
    }
632
633
    $ns   = getNS($id);
634
    $perm = -1;
635
636
    //add ALL group
637
    $groups[] = '@ALL';
638
639
    //add User
640
    if($user) $groups[] = $user;
641
642
    //check exact match first
643
    $matches = preg_grep('/^'.preg_quote($id, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
644
    if(count($matches)) {
645
        foreach($matches as $match) {
646
            $match = preg_replace('/#.*$/', '', $match); //ignore comments
647
            $acl   = preg_split('/[ \t]+/', $match);
648
            if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
649
                $acl[1] = utf8_strtolower($acl[1]);
650
            }
651
            if(!in_array($acl[1], $groups)) {
652
                continue;
653
            }
654
            if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
655
            if($acl[2] > $perm) {
656
                $perm = $acl[2];
657
            }
658
        }
659
        if($perm > -1) {
660
            //we had a match - return it
661
            return (int) $perm;
662
        }
663
    }
664
665
    //still here? do the namespace checks
666
    if($ns) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ns of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
667
        $path = $ns.':*';
668
    } else {
669
        $path = '*'; //root document
670
    }
671
672
    do {
673
        $matches = preg_grep('/^'.preg_quote($path, '/').'[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
674
        if(count($matches)) {
675
            foreach($matches as $match) {
676
                $match = preg_replace('/#.*$/', '', $match); //ignore comments
677
                $acl   = preg_split('/[ \t]+/', $match);
678
                if(!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
679
                    $acl[1] = utf8_strtolower($acl[1]);
680
                }
681
                if(!in_array($acl[1], $groups)) {
682
                    continue;
683
                }
684
                if($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
685
                if($acl[2] > $perm) {
686
                    $perm = $acl[2];
687
                }
688
            }
689
            //we had a match - return it
690
            if($perm != -1) {
691
                return (int) $perm;
692
            }
693
        }
694
        //get next higher namespace
695
        $ns = getNS($ns);
0 ignored issues
show
Security Bug introduced by
It seems like $ns defined by getNS($ns) on line 695 can also be of type false; however, getNS() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
696
697
        if($path != '*') {
698
            $path = $ns.':*';
699
            if($path == ':*') $path = '*';
700
        } else {
701
            //we did this already
702
            //looks like there is something wrong with the ACL
703
            //break here
704
            msg('No ACL setup yet! Denying access to everyone.');
705
            return AUTH_NONE;
706
        }
707
    } while(1); //this should never loop endless
708
    return AUTH_NONE;
709
}
710
711
/**
712
 * Encode ASCII special chars
713
 *
714
 * Some auth backends allow special chars in their user and groupnames
715
 * The special chars are encoded with this function. Only ASCII chars
716
 * are encoded UTF-8 multibyte are left as is (different from usual
717
 * urlencoding!).
718
 *
719
 * Decoding can be done with rawurldecode
720
 *
721
 * @author Andreas Gohr <[email protected]>
722
 * @see rawurldecode()
723
 *
724
 * @param string $name
725
 * @param bool $skip_group
726
 * @return string
727
 */
728
function auth_nameencode($name, $skip_group = false) {
729
    global $cache_authname;
730
    $cache =& $cache_authname;
731
    $name  = (string) $name;
732
733
    // never encode wildcard FS#1955
734
    if($name == '%USER%') return $name;
735
    if($name == '%GROUP%') return $name;
736
737
    if(!isset($cache[$name][$skip_group])) {
738
        if($skip_group && $name{0} == '@') {
739
            $cache[$name][$skip_group] = '@'.preg_replace_callback(
740
                '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
741
                'auth_nameencode_callback', substr($name, 1)
742
            );
743
        } else {
744
            $cache[$name][$skip_group] = preg_replace_callback(
745
                '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
746
                'auth_nameencode_callback', $name
747
            );
748
        }
749
    }
750
751
    return $cache[$name][$skip_group];
752
}
753
754
/**
755
 * callback encodes the matches
756
 *
757
 * @param array $matches first complete match, next matching subpatterms
758
 * @return string
759
 */
760
function auth_nameencode_callback($matches) {
761
    return '%'.dechex(ord(substr($matches[1],-1)));
762
}
763
764
/**
765
 * Create a pronouncable password
766
 *
767
 * The $foruser variable might be used by plugins to run additional password
768
 * policy checks, but is not used by the default implementation
769
 *
770
 * @author   Andreas Gohr <[email protected]>
771
 * @link     http://www.phpbuilder.com/annotate/message.php3?id=1014451
772
 * @triggers AUTH_PASSWORD_GENERATE
773
 *
774
 * @param  string $foruser username for which the password is generated
775
 * @return string  pronouncable password
776
 */
777
function auth_pwgen($foruser = '') {
778
    $data = array(
779
        'password' => '',
780
        'foruser'  => $foruser
781
    );
782
783
    $evt = new Event('AUTH_PASSWORD_GENERATE', $data);
784
    if($evt->advise_before(true)) {
785
        $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
786
        $v = 'aeiou'; //vowels
787
        $a = $c.$v; //both
788
        $s = '!$%&?+*~#-_:.;,'; // specials
789
790
        //use thre syllables...
791
        for($i = 0; $i < 3; $i++) {
792
            $data['password'] .= $c[auth_random(0, strlen($c) - 1)];
793
            $data['password'] .= $v[auth_random(0, strlen($v) - 1)];
794
            $data['password'] .= $a[auth_random(0, strlen($a) - 1)];
795
        }
796
        //... and add a nice number and special
797
        $data['password'] .= auth_random(10, 99).$s[auth_random(0, strlen($s) - 1)];
798
    }
799
    $evt->advise_after();
800
801
    return $data['password'];
802
}
803
804
/**
805
 * Sends a password to the given user
806
 *
807
 * @author  Andreas Gohr <[email protected]>
808
 *
809
 * @param string $user Login name of the user
810
 * @param string $password The new password in clear text
811
 * @return bool  true on success
812
 */
813
function auth_sendPassword($user, $password) {
814
    global $lang;
815
    /* @var AuthPlugin $auth */
816
    global $auth;
817
    if(!$auth) return false;
818
819
    $user     = $auth->cleanUser($user);
820
    $userinfo = $auth->getUserData($user, $requireGroups = false);
821
822
    if(!$userinfo['mail']) return false;
823
824
    $text = rawLocale('password');
825
    $trep = array(
826
        'FULLNAME' => $userinfo['name'],
827
        'LOGIN'    => $user,
828
        'PASSWORD' => $password
829
    );
830
831
    $mail = new Mailer();
832
    $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>');
833
    $mail->subject($lang['regpwmail']);
834
    $mail->setBody($text, $trep);
835
    return $mail->send();
836
}
837
838
/**
839
 * Register a new user
840
 *
841
 * This registers a new user - Data is read directly from $_POST
842
 *
843
 * @author  Andreas Gohr <[email protected]>
844
 *
845
 * @return bool  true on success, false on any error
846
 */
847
function register() {
848
    global $lang;
849
    global $conf;
850
    /* @var \dokuwiki\Extension\AuthPlugin $auth */
851
    global $auth;
852
    global $INPUT;
853
854
    if(!$INPUT->post->bool('save')) return false;
855
    if(!actionOK('register')) return false;
856
857
    // gather input
858
    $login    = trim($auth->cleanUser($INPUT->post->str('login')));
859
    $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname')));
860
    $email    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email')));
861
    $pass     = $INPUT->post->str('pass');
862
    $passchk  = $INPUT->post->str('passchk');
863
864
    if(empty($login) || empty($fullname) || empty($email)) {
865
        msg($lang['regmissing'], -1);
866
        return false;
867
    }
868
869
    if($conf['autopasswd']) {
870
        $pass = auth_pwgen($login); // automatically generate password
871
    } elseif(empty($pass) || empty($passchk)) {
872
        msg($lang['regmissing'], -1); // complain about missing passwords
873
        return false;
874
    } elseif($pass != $passchk) {
875
        msg($lang['regbadpass'], -1); // complain about misspelled passwords
876
        return false;
877
    }
878
879
    //check mail
880
    if(!mail_isvalid($email)) {
881
        msg($lang['regbadmail'], -1);
882
        return false;
883
    }
884
885
    //okay try to create the user
886
    if(!$auth->triggerUserMod('create', array($login, $pass, $fullname, $email))) {
887
        msg($lang['regfail'], -1);
888
        return false;
889
    }
890
891
    // send notification about the new user
892
    $subscription = new Subscription();
893
    $subscription->send_register($login, $fullname, $email);
894
895
    // are we done?
896
    if(!$conf['autopasswd']) {
897
        msg($lang['regsuccess2'], 1);
898
        return true;
899
    }
900
901
    // autogenerated password? then send password to user
902
    if(auth_sendPassword($login, $pass)) {
903
        msg($lang['regsuccess'], 1);
904
        return true;
905
    } else {
906
        msg($lang['regmailfail'], -1);
907
        return false;
908
    }
909
}
910
911
/**
912
 * Update user profile
913
 *
914
 * @author    Christopher Smith <[email protected]>
915
 */
916
function updateprofile() {
917
    global $conf;
918
    global $lang;
919
    /* @var AuthPlugin $auth */
920
    global $auth;
921
    /* @var Input $INPUT */
922
    global $INPUT;
923
924
    if(!$INPUT->post->bool('save')) return false;
925
    if(!checkSecurityToken()) return false;
926
927
    if(!actionOK('profile')) {
928
        msg($lang['profna'], -1);
929
        return false;
930
    }
931
932
    $changes         = array();
933
    $changes['pass'] = $INPUT->post->str('newpass');
934
    $changes['name'] = $INPUT->post->str('fullname');
935
    $changes['mail'] = $INPUT->post->str('email');
936
937
    // check misspelled passwords
938
    if($changes['pass'] != $INPUT->post->str('passchk')) {
939
        msg($lang['regbadpass'], -1);
940
        return false;
941
    }
942
943
    // clean fullname and email
944
    $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
945
    $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
946
947
    // no empty name and email (except the backend doesn't support them)
948
    if((empty($changes['name']) && $auth->canDo('modName')) ||
949
        (empty($changes['mail']) && $auth->canDo('modMail'))
950
    ) {
951
        msg($lang['profnoempty'], -1);
952
        return false;
953
    }
954
    if(!mail_isvalid($changes['mail']) && $auth->canDo('modMail')) {
955
        msg($lang['regbadmail'], -1);
956
        return false;
957
    }
958
959
    $changes = array_filter($changes);
960
961
    // check for unavailable capabilities
962
    if(!$auth->canDo('modName')) unset($changes['name']);
963
    if(!$auth->canDo('modMail')) unset($changes['mail']);
964
    if(!$auth->canDo('modPass')) unset($changes['pass']);
965
966
    // anything to do?
967
    if(!count($changes)) {
968
        msg($lang['profnochange'], -1);
969
        return false;
970
    }
971
972
    if($conf['profileconfirm']) {
973
        if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
974
            msg($lang['badpassconfirm'], -1);
975
            return false;
976
        }
977
    }
978
979
    if(!$auth->triggerUserMod('modify', array($INPUT->server->str('REMOTE_USER'), &$changes))) {
980
        msg($lang['proffail'], -1);
981
        return false;
982
    }
983
984
    if($changes['pass']) {
985
        // update cookie and session with the changed data
986
        list( /*user*/, $sticky, /*pass*/) = auth_getCookie();
987
        $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true));
0 ignored issues
show
Bug introduced by
It seems like auth_cookiesalt(!$sticky, true) targeting auth_cookiesalt() can also be of type boolean; however, auth_encrypt() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
988
        auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky);
989
    } else {
990
        // make sure the session is writable
991
        @session_start();
992
        // invalidate session cache
993
        $_SESSION[DOKU_COOKIE]['auth']['time'] = 0;
994
        session_write_close();
995
    }
996
997
    return true;
998
}
999
1000
/**
1001
 * Delete the current logged-in user
1002
 *
1003
 * @return bool true on success, false on any error
1004
 */
1005
function auth_deleteprofile(){
1006
    global $conf;
1007
    global $lang;
1008
    /* @var \dokuwiki\Extension\AuthPlugin $auth */
1009
    global $auth;
1010
    /* @var Input $INPUT */
1011
    global $INPUT;
1012
1013
    if(!$INPUT->post->bool('delete')) return false;
1014
    if(!checkSecurityToken()) return false;
1015
1016
    // action prevented or auth module disallows
1017
    if(!actionOK('profile_delete') || !$auth->canDo('delUser')) {
1018
        msg($lang['profnodelete'], -1);
1019
        return false;
1020
    }
1021
1022
    if(!$INPUT->post->bool('confirm_delete')){
1023
        msg($lang['profconfdeletemissing'], -1);
1024
        return false;
1025
    }
1026
1027
    if($conf['profileconfirm']) {
1028
        if(!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1029
            msg($lang['badpassconfirm'], -1);
1030
            return false;
1031
        }
1032
    }
1033
1034
    $deleted = array();
1035
    $deleted[] = $INPUT->server->str('REMOTE_USER');
1036
    if($auth->triggerUserMod('delete', array($deleted))) {
1037
        // force and immediate logout including removing the sticky cookie
1038
        auth_logoff();
1039
        return true;
1040
    }
1041
1042
    return false;
1043
}
1044
1045
/**
1046
 * Send a  new password
1047
 *
1048
 * This function handles both phases of the password reset:
1049
 *
1050
 *   - handling the first request of password reset
1051
 *   - validating the password reset auth token
1052
 *
1053
 * @author Benoit Chesneau <[email protected]>
1054
 * @author Chris Smith <[email protected]>
1055
 * @author Andreas Gohr <[email protected]>
1056
 *
1057
 * @return bool true on success, false on any error
1058
 */
1059
function act_resendpwd() {
1060
    global $lang;
1061
    global $conf;
1062
    /* @var AuthPlugin $auth */
1063
    global $auth;
1064
    /* @var Input $INPUT */
1065
    global $INPUT;
1066
1067
    if(!actionOK('resendpwd')) {
1068
        msg($lang['resendna'], -1);
1069
        return false;
1070
    }
1071
1072
    $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
1073
1074
    if($token) {
1075
        // we're in token phase - get user info from token
1076
1077
        $tfile = $conf['cachedir'].'/'.$token{0}.'/'.$token.'.pwauth';
1078
        if(!file_exists($tfile)) {
1079
            msg($lang['resendpwdbadauth'], -1);
1080
            $INPUT->remove('pwauth');
1081
            return false;
1082
        }
1083
        // token is only valid for 3 days
1084
        if((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
1085
            msg($lang['resendpwdbadauth'], -1);
1086
            $INPUT->remove('pwauth');
1087
            @unlink($tfile);
1088
            return false;
1089
        }
1090
1091
        $user     = io_readfile($tfile);
1092
        $userinfo = $auth->getUserData($user, $requireGroups = false);
1093
        if(!$userinfo['mail']) {
1094
            msg($lang['resendpwdnouser'], -1);
1095
            return false;
1096
        }
1097
1098
        if(!$conf['autopasswd']) { // we let the user choose a password
1099
            $pass = $INPUT->str('pass');
1100
1101
            // password given correctly?
1102
            if(!$pass) return false;
1103
            if($pass != $INPUT->str('passchk')) {
1104
                msg($lang['regbadpass'], -1);
1105
                return false;
1106
            }
1107
1108
            // change it
1109
            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
1110
                msg($lang['proffail'], -1);
1111
                return false;
1112
            }
1113
1114
        } else { // autogenerate the password and send by mail
1115
1116
            $pass = auth_pwgen($user);
1117
            if(!$auth->triggerUserMod('modify', array($user, array('pass' => $pass)))) {
1118
                msg($lang['proffail'], -1);
1119
                return false;
1120
            }
1121
1122
            if(auth_sendPassword($user, $pass)) {
1123
                msg($lang['resendpwdsuccess'], 1);
1124
            } else {
1125
                msg($lang['regmailfail'], -1);
1126
            }
1127
        }
1128
1129
        @unlink($tfile);
1130
        return true;
1131
1132
    } else {
1133
        // we're in request phase
1134
1135
        if(!$INPUT->post->bool('save')) return false;
1136
1137
        if(!$INPUT->post->str('login')) {
1138
            msg($lang['resendpwdmissing'], -1);
1139
            return false;
1140
        } else {
1141
            $user = trim($auth->cleanUser($INPUT->post->str('login')));
1142
        }
1143
1144
        $userinfo = $auth->getUserData($user, $requireGroups = false);
1145
        if(!$userinfo['mail']) {
1146
            msg($lang['resendpwdnouser'], -1);
1147
            return false;
1148
        }
1149
1150
        // generate auth token
1151
        $token = md5(auth_randombytes(16)); // random secret
1152
        $tfile = $conf['cachedir'].'/'.$token{0}.'/'.$token.'.pwauth';
1153
        $url   = wl('', array('do'=> 'resendpwd', 'pwauth'=> $token), true, '&');
1154
1155
        io_saveFile($tfile, $user);
1156
1157
        $text = rawLocale('pwconfirm');
1158
        $trep = array(
1159
            'FULLNAME' => $userinfo['name'],
1160
            'LOGIN'    => $user,
1161
            'CONFIRM'  => $url
1162
        );
1163
1164
        $mail = new Mailer();
1165
        $mail->to($userinfo['name'].' <'.$userinfo['mail'].'>');
1166
        $mail->subject($lang['regpwmail']);
1167
        $mail->setBody($text, $trep);
1168
        if($mail->send()) {
1169
            msg($lang['resendpwdconfirm'], 1);
1170
        } else {
1171
            msg($lang['regmailfail'], -1);
1172
        }
1173
        return true;
1174
    }
1175
    // never reached
1176
}
1177
1178
/**
1179
 * Encrypts a password using the given method and salt
1180
 *
1181
 * If the selected method needs a salt and none was given, a random one
1182
 * is chosen.
1183
 *
1184
 * @author  Andreas Gohr <[email protected]>
1185
 *
1186
 * @param string $clear The clear text password
1187
 * @param string $method The hashing method
1188
 * @param string $salt A salt, null for random
1189
 * @return  string  The crypted password
1190
 */
1191
function auth_cryptPassword($clear, $method = '', $salt = null) {
1192
    global $conf;
1193
    if(empty($method)) $method = $conf['passcrypt'];
1194
1195
    $pass = new PassHash();
1196
    $call = 'hash_'.$method;
1197
1198
    if(!method_exists($pass, $call)) {
1199
        msg("Unsupported crypt method $method", -1);
1200
        return false;
1201
    }
1202
1203
    return $pass->$call($clear, $salt);
1204
}
1205
1206
/**
1207
 * Verifies a cleartext password against a crypted hash
1208
 *
1209
 * @author Andreas Gohr <[email protected]>
1210
 *
1211
 * @param  string $clear The clear text password
1212
 * @param  string $crypt The hash to compare with
1213
 * @return bool true if both match
1214
 */
1215
function auth_verifyPassword($clear, $crypt) {
1216
    $pass = new PassHash();
1217
    return $pass->verify_hash($clear, $crypt);
1218
}
1219
1220
/**
1221
 * Set the authentication cookie and add user identification data to the session
1222
 *
1223
 * @param string  $user       username
1224
 * @param string  $pass       encrypted password
1225
 * @param bool    $sticky     whether or not the cookie will last beyond the session
1226
 * @return bool
1227
 */
1228
function auth_setCookie($user, $pass, $sticky) {
1229
    global $conf;
1230
    /* @var AuthPlugin $auth */
1231
    global $auth;
1232
    global $USERINFO;
1233
1234
    if(!$auth) return false;
1235
    $USERINFO = $auth->getUserData($user);
1236
1237
    // set cookie
1238
    $cookie    = base64_encode($user).'|'.((int) $sticky).'|'.base64_encode($pass);
1239
    $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1240
    $time      = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
1241
    setcookie(DOKU_COOKIE, $cookie, $time, $cookieDir, '', ($conf['securecookie'] && is_ssl()), true);
1242
1243
    // set session
1244
    $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
1245
    $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
1246
    $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
1247
    $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
1248
    $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
1249
1250
    return true;
1251
}
1252
1253
/**
1254
 * Returns the user, (encrypted) password and sticky bit from cookie
1255
 *
1256
 * @returns array
1257
 */
1258
function auth_getCookie() {
1259
    if(!isset($_COOKIE[DOKU_COOKIE])) {
1260
        return array(null, null, null);
1261
    }
1262
    list($user, $sticky, $pass) = explode('|', $_COOKIE[DOKU_COOKIE], 3);
1263
    $sticky = (bool) $sticky;
1264
    $pass   = base64_decode($pass);
1265
    $user   = base64_decode($user);
1266
    return array($user, $sticky, $pass);
1267
}
1268
1269
//Setup VIM: ex: et ts=2 :
1270