Failed Conditions
Push — psr2-pluginredux ( 89614c )
by Andreas
05:43 queued 03:04
created

auth.php ➔ auth_isadmin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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