Completed
Push — remoteapiGetversions ( 33fe8b...b2f4ab )
by Gerrit
12s
created

auth_plugin_authmysql::leaveGroup()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 11
rs 9.4285
cc 2
eloc 8
nc 2
nop 2
1
<?php
2
// must be run within Dokuwiki
3
if(!defined('DOKU_INC')) die();
4
5
/**
6
 * MySQL authentication backend
7
 *
8
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9
 * @author     Andreas Gohr <[email protected]>
10
 * @author     Chris Smith <[email protected]>
11
 * @author     Matthias Grimm <[email protected]>
12
 * @author     Jan Schumann <[email protected]>
13
 */
14
class auth_plugin_authmysql extends DokuWiki_Auth_Plugin {
15
    /** @var resource holds the database connection */
16
    protected $dbcon = 0;
17
    /** @var int database version*/
18
    protected $dbver = 0;
19
    /** @var int database revision */
20
    protected $dbrev = 0;
21
    /** @var int database subrevision */
22
    protected $dbsub = 0;
23
24
    /** @var array cache to avoid re-reading user info data */
25
    protected $cacheUserInfo = array();
26
27
    /**
28
     * Constructor
29
     *
30
     * checks if the mysql interface is available, otherwise it will
31
     * set the variable $success of the basis class to false
32
     *
33
     * @author Matthias Grimm <[email protected]>
34
     */
35
    public function __construct() {
36
        parent::__construct();
37
38
        if(!function_exists('mysql_connect')) {
39
            $this->_debug("MySQL err: PHP MySQL extension not found.", -1, __LINE__, __FILE__);
40
            $this->success = false;
41
            return;
42
        }
43
44
        // set capabilities based upon config strings set
45
        if(!$this->getConf('server') || !$this->getConf('user') || !$this->getConf('database')) {
46
            $this->_debug("MySQL err: insufficient configuration.", -1, __LINE__, __FILE__);
47
48
            $this->success = false;
49
            return;
50
        }
51
52
        $this->cando['addUser']   = $this->_chkcnf(
53
            array(
54
                 'getUserInfo',
55
                 'getGroups',
56
                 'addUser',
57
                 'getUserID',
58
                 'getGroupID',
59
                 'addGroup',
60
                 'addUserGroup'
61
            ), true
62
        );
63
        $this->cando['delUser']   = $this->_chkcnf(
64
            array(
65
                 'getUserID',
66
                 'delUser',
67
                 'delUserRefs'
68
            ), true
69
        );
70
        $this->cando['modLogin']  = $this->_chkcnf(
71
            array(
72
                 'getUserID',
73
                 'updateUser',
74
                 'UpdateTarget'
75
            ), true
76
        );
77
        $this->cando['modPass']   = $this->cando['modLogin'];
78
        $this->cando['modName']   = $this->cando['modLogin'];
79
        $this->cando['modMail']   = $this->cando['modLogin'];
80
        $this->cando['modGroups'] = $this->_chkcnf(
81
            array(
82
                 'getUserID',
83
                 'getGroups',
84
                 'getGroupID',
85
                 'addGroup',
86
                 'addUserGroup',
87
                 'delGroup',
88
                 'getGroupID',
89
                 'delUserGroup'
90
            ), true
91
        );
92
        /* getGroups is not yet supported
93
           $this->cando['getGroups']    = $this->_chkcnf(array('getGroups',
94
           'getGroupID'),false); */
95
        $this->cando['getUsers']     = $this->_chkcnf(
96
            array(
97
                 'getUsers',
98
                 'getUserInfo',
99
                 'getGroups'
100
            ), false
101
        );
102
        $this->cando['getUserCount'] = $this->_chkcnf(array('getUsers'), false);
103
104
        if($this->getConf('debug') >= 2) {
105
            $candoDebug = '';
106
            foreach($this->cando as $cd => $value) {
107
                if($value) { $value = 'yes'; } else { $value = 'no'; }
108
                $candoDebug .= $cd . ": " . $value . " | ";
109
            }
110
            $this->_debug("authmysql cando: " . $candoDebug, 0, __LINE__, __FILE__);
111
        }
112
    }
113
114
    /**
115
     * Check if the given config strings are set
116
     *
117
     * @author  Matthias Grimm <[email protected]>
118
     *
119
     * @param   string[] $keys
120
     * @param   bool  $wop is this a check for a write operation?
121
     * @return  bool
122
     */
123
    protected function _chkcnf($keys, $wop = false) {
124
        foreach($keys as $key) {
125
            if(!$this->getConf($key)) return false;
126
        }
127
128
        /* write operation and lock array filled with tables names? */
129
        if($wop && (!is_array($this->getConf('TablesToLock')) ||
130
            !count($this->getConf('TablesToLock')))
131
        ) {
132
            return false;
133
        }
134
135
        return true;
136
    }
137
138
    /**
139
     * Checks if the given user exists and the given plaintext password
140
     * is correct. Furtheron it might be checked wether the user is
141
     * member of the right group
142
     *
143
     * Depending on which SQL string is defined in the config, password
144
     * checking is done here (getpass) or by the database (passcheck)
145
     *
146
     * @param  string $user user who would like access
147
     * @param  string $pass user's clear text password to check
148
     * @return bool
149
     *
150
     * @author  Andreas Gohr <[email protected]>
151
     * @author  Matthias Grimm <[email protected]>
152
     */
153
    public function checkPass($user, $pass) {
154
        global $conf;
155
        $rc = false;
156
157
        if($this->_openDB()) {
158
            $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('checkPass'));
159
            $sql    = str_replace('%{pass}', $this->_escape($pass), $sql);
160
            $sql    = str_replace('%{dgroup}', $this->_escape($conf['defaultgroup']), $sql);
161
            $result = $this->_queryDB($sql);
162
163
            if($result !== false && count($result) == 1) {
164
                if($this->getConf('forwardClearPass') == 1) {
165
                    $rc = true;
166
                } else {
167
                    $rc = auth_verifyPassword($pass, $result[0]['pass']);
168
                }
169
            }
170
            $this->_closeDB();
171
        }
172
        return $rc;
173
    }
174
175
    /**
176
     * Return user info
177
     *
178
     * @author  Andreas Gohr <[email protected]>
179
     * @author  Matthias Grimm <[email protected]>
180
     *
181
     * @param string $user user login to get data for
182
     * @param bool $requireGroups  when true, group membership information should be included in the returned array;
183
     *                             when false, it maybe included, but is not required by the caller
184
     * @return array|bool
185
     */
186
    public function getUserData($user, $requireGroups=true) {
187
        if($this->_cacheExists($user, $requireGroups)) {
188
            return $this->cacheUserInfo[$user];
189
        }
190
191
        if($this->_openDB()) {
192
            $this->_lockTables("READ");
193
            $info = $this->_getUserInfo($user, $requireGroups);
194
            $this->_unlockTables();
195
            $this->_closeDB();
196
        } else {
197
            $info = false;
198
        }
199
        return $info;
200
    }
201
202
    /**
203
     * Create a new User. Returns false if the user already exists,
204
     * null when an error occurred and true if everything went well.
205
     *
206
     * The new user will be added to the default group by this
207
     * function if grps are not specified (default behaviour).
208
     *
209
     * @author  Andreas Gohr <[email protected]>
210
     * @author  Chris Smith <[email protected]>
211
     * @author  Matthias Grimm <[email protected]>
212
     *
213
     * @param string $user  nick of the user
214
     * @param string $pwd   clear text password
215
     * @param string $name  full name of the user
216
     * @param string $mail  email address
217
     * @param array  $grps  array of groups the user should become member of
218
     * @return bool|null
219
     */
220
    public function createUser($user, $pwd, $name, $mail, $grps = null) {
221
        global $conf;
222
223
        if($this->_openDB()) {
224
            if(($info = $this->_getUserInfo($user)) !== false) {
225
                msg($this->getLang('userexists'), -1);
226
                return false; // user already exists
227
            }
228
229
            // set defaultgroup if no groups were given
230
            if($grps == null) {
231
                $grps = array($conf['defaultgroup']);
232
            }
233
234
            $this->_lockTables("WRITE");
235
            $pwd = $this->getConf('forwardClearPass') ? $pwd : auth_cryptPassword($pwd);
236
            $rc  = $this->_addUser($user, $pwd, $name, $mail, $grps);
237
            $this->_unlockTables();
238
            $this->_closeDB();
239
            if(!$rc) {
240
                msg($this->getLang('writefail'));
241
                return null;
242
            }
243
            return true;
244
        } else {
245
            msg($this->getLang('connectfail'), -1);
246
        }
247
        return null; // return error
248
    }
249
250
    /**
251
     * Modify user data
252
     *
253
     * An existing user dataset will be modified. Changes are given in an array.
254
     *
255
     * The dataset update will be rejected if the user name should be changed
256
     * to an already existing one.
257
     *
258
     * The password must be provided unencrypted. Pasword encryption is done
259
     * automatically if configured.
260
     *
261
     * If one or more groups can't be updated, an error will be set. In
262
     * this case the dataset might already be changed and we can't rollback
263
     * the changes. Transactions would be really useful here.
264
     *
265
     * modifyUser() may be called without SQL statements defined that are
266
     * needed to change group membership (for example if only the user profile
267
     * should be modified). In this case we assure that we don't touch groups
268
     * even when $changes['grps'] is set by mistake.
269
     *
270
     * @author  Chris Smith <[email protected]>
271
     * @author  Matthias Grimm <[email protected]>
272
     *
273
     * @param   string $user    nick of the user to be changed
274
     * @param   array  $changes array of field/value pairs to be changed (password will be clear text)
275
     * @return  bool   true on success, false on error
276
     */
277
    public function modifyUser($user, $changes) {
278
        $rc = false;
279
280
        if(!is_array($changes) || !count($changes)) {
281
            return true; // nothing to change
282
        }
283
284
        if($this->_openDB()) {
285
            $this->_lockTables("WRITE");
286
287
            $rc = $this->_updateUserInfo($user, $changes);
288
289
            if(!$rc) {
290
                msg($this->getLang('usernotexists'), -1);
291
            } elseif(isset($changes['grps']) && $this->cando['modGroups']) {
292
                $groups = $this->_getGroups($user);
293
                $grpadd = array_diff($changes['grps'], $groups);
294
                $grpdel = array_diff($groups, $changes['grps']);
295
296
                foreach($grpadd as $group) {
297
                    if(($this->_addUserToGroup($user, $group, true)) == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
298
                        $rc = false;
299
                    }
300
                }
301
302
                foreach($grpdel as $group) {
303
                    if(($this->_delUserFromGroup($user, $group)) == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
304
                        $rc = false;
305
                    }
306
                }
307
308
                if(!$rc) msg($this->getLang('writefail'));
309
            }
310
311
            $this->_unlockTables();
312
            $this->_closeDB();
313
        } else {
314
            msg($this->getLang('connectfail'), -1);
315
        }
316
        return $rc;
317
    }
318
319
    /**
320
     * [public function]
321
     *
322
     * Remove one or more users from the list of registered users
323
     *
324
     * @param   array  $users   array of users to be deleted
325
     * @return  int             the number of users deleted
326
     *
327
     * @author  Christopher Smith <[email protected]>
328
     * @author  Matthias Grimm <[email protected]>
329
     */
330
    function deleteUsers($users) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
331
        $count = 0;
332
333
        if($this->_openDB()) {
334
            if(is_array($users) && count($users)) {
335
                $this->_lockTables("WRITE");
336
                foreach($users as $user) {
337
                    if($this->_delUser($user)) {
338
                        $count++;
339
                    }
340
                }
341
                $this->_unlockTables();
342
            }
343
            $this->_closeDB();
344
        } else {
345
            msg($this->getLang('connectfail'), -1);
346
        }
347
        return $count;
348
    }
349
350
    /**
351
     * Counts users which meet certain $filter criteria.
352
     *
353
     * @author  Matthias Grimm <[email protected]>
354
     *
355
     * @param  array $filter  filter criteria in item/pattern pairs
356
     * @return int count of found users
357
     */
358
    public function getUserCount($filter = array()) {
359
        $rc = 0;
360
361
        if($this->_openDB()) {
362
            $sql = $this->_createSQLFilter($this->getConf('getUsers'), $filter);
363
364
            if($this->dbver >= 4) {
365
                $sql = substr($sql, 6); /* remove 'SELECT' or 'select' */
366
                $sql = "SELECT SQL_CALC_FOUND_ROWS".$sql." LIMIT 1";
367
                $this->_queryDB($sql);
368
                $result = $this->_queryDB("SELECT FOUND_ROWS()");
369
                $rc     = $result[0]['FOUND_ROWS()'];
370
            } else if(($result = $this->_queryDB($sql)))
371
                $rc = count($result);
372
373
            $this->_closeDB();
374
        }
375
        return $rc;
376
    }
377
378
    /**
379
     * Bulk retrieval of user data
380
     *
381
     * @author  Matthias Grimm <[email protected]>
382
     *
383
     * @param  int          $first  index of first user to be returned
384
     * @param  int          $limit  max number of users to be returned
385
     * @param  array $filter array of field/pattern pairs
386
     * @return  array userinfo (refer getUserData for internal userinfo details)
387
     */
388
    public function retrieveUsers($first = 0, $limit = 0, $filter = array()) {
389
        $out = array();
390
391
        if($this->_openDB()) {
392
            $this->_lockTables("READ");
393
            $sql = $this->_createSQLFilter($this->getConf('getUsers'), $filter);
394
            $sql .= " ".$this->getConf('SortOrder');
395
            if($limit) {
396
                $sql .= " LIMIT $first, $limit";
397
            } elseif($first) {
398
                $sql .= " LIMIT $first";
399
            }
400
            $result = $this->_queryDB($sql);
401
402
            if(!empty($result)) {
403
                foreach($result as $user) {
404
                    if(($info = $this->_getUserInfo($user['user']))) {
405
                        $out[$user['user']] = $info;
406
                    }
407
                }
408
            }
409
410
            $this->_unlockTables();
411
            $this->_closeDB();
412
        }
413
        return $out;
414
    }
415
416
    /**
417
     * Give user membership of a group
418
     *
419
     * @author  Matthias Grimm <[email protected]>
420
     *
421
     * @param   string $user
422
     * @param   string $group
423
     * @return  bool   true on success, false on error
424
     */
425
    protected function joinGroup($user, $group) {
426
        $rc = false;
427
428
        if($this->_openDB()) {
429
            $this->_lockTables("WRITE");
430
            $rc = $this->_addUserToGroup($user, $group);
431
            $this->_unlockTables();
432
            $this->_closeDB();
433
        }
434
        return $rc;
435
    }
436
437
    /**
438
     * Remove user from a group
439
     *
440
     * @author  Matthias Grimm <[email protected]>
441
     *
442
     * @param   string $user  user that leaves a group
443
     * @param   string $group group to leave
444
     * @return  bool
445
     */
446
    protected function leaveGroup($user, $group) {
447
        $rc = false;
448
449
        if($this->_openDB()) {
450
            $this->_lockTables("WRITE");
451
            $rc  = $this->_delUserFromGroup($user, $group);
452
            $this->_unlockTables();
453
            $this->_closeDB();
454
        }
455
        return $rc;
456
    }
457
458
    /**
459
     * MySQL is case-insensitive
460
     */
461
    public function isCaseSensitive() {
462
        return false;
463
    }
464
465
    /**
466
     * Adds a user to a group.
467
     *
468
     * If $force is set to true non existing groups would be created.
469
     *
470
     * The database connection must already be established. Otherwise
471
     * this function does nothing and returns 'false'. It is strongly
472
     * recommended to call this function only after all participating
473
     * tables (group and usergroup) have been locked.
474
     *
475
     * @author Matthias Grimm <[email protected]>
476
     *
477
     * @param   string $user    user to add to a group
478
     * @param   string $group   name of the group
479
     * @param   bool   $force   create missing groups
480
     * @return  bool   true on success, false on error
481
     */
482
    protected function _addUserToGroup($user, $group, $force = false) {
483
        $newgroup = 0;
484
485
        if(($this->dbcon) && ($user)) {
486
            $gid = $this->_getGroupID($group);
487
            if(!$gid) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gid of type false|string is loosely compared to false; 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...
488
                if($force) { // create missing groups
489
                    $sql      = str_replace('%{group}', $this->_escape($group), $this->getConf('addGroup'));
490
                    $gid      = $this->_modifyDB($sql);
491
                    $newgroup = 1; // group newly created
492
                }
493
                if(!$gid) return false; // group didn't exist and can't be created
494
            }
495
496
            $sql = $this->getConf('addUserGroup');
497
            if(strpos($sql, '%{uid}') !== false) {
498
                $uid = $this->_getUserID($user);
499
                $sql = str_replace('%{uid}', $this->_escape($uid), $sql);
500
            }
501
            $sql = str_replace('%{user}', $this->_escape($user), $sql);
502
            $sql = str_replace('%{gid}', $this->_escape($gid), $sql);
503
            $sql = str_replace('%{group}', $this->_escape($group), $sql);
504
            if($this->_modifyDB($sql) !== false) {
505
                $this->_flushUserInfoCache($user);
506
                return true;
507
            }
508
509
            if($newgroup) { // remove previously created group on error
510
                $sql = str_replace('%{gid}', $this->_escape($gid), $this->getConf('delGroup'));
511
                $sql = str_replace('%{group}', $this->_escape($group), $sql);
512
                $this->_modifyDB($sql);
513
            }
514
        }
515
        return false;
516
    }
517
518
    /**
519
     * Remove user from a group
520
     *
521
     * @author  Matthias Grimm <[email protected]>
522
     *
523
     * @param   string $user  user that leaves a group
524
     * @param   string $group group to leave
525
     * @return  bool   true on success, false on error
526
     */
527
    protected function _delUserFromGroup($user, $group) {
528
        $rc = false;
529
530
        if(($this->dbcon) && ($user)) {
531
            $sql = $this->getConf('delUserGroup');
532
            if(strpos($sql, '%{uid}') !== false) {
533
                $uid = $this->_getUserID($user);
534
                $sql = str_replace('%{uid}', $this->_escape($uid), $sql);
535
            }
536
            $gid = $this->_getGroupID($group);
537
            if($gid) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $gid of type false|string 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...
538
                $sql = str_replace('%{user}', $this->_escape($user), $sql);
539
                $sql = str_replace('%{gid}', $this->_escape($gid), $sql);
540
                $sql = str_replace('%{group}', $this->_escape($group), $sql);
541
                $rc  = $this->_modifyDB($sql) == 0 ? true : false;
542
543
                if ($rc) {
544
                    $this->_flushUserInfoCache($user);
545
                }
546
            }
547
        }
548
        return $rc;
549
    }
550
551
    /**
552
     * Retrieves a list of groups the user is a member off.
553
     *
554
     * The database connection must already be established
555
     * for this function to work. Otherwise it will return
556
     * false.
557
     *
558
     * @author Matthias Grimm <[email protected]>
559
     *
560
     * @param  string $user user whose groups should be listed
561
     * @return bool|array false on error, all groups on success
562
     */
563
    protected function _getGroups($user) {
564
        $groups = array();
565
566
        if($this->dbcon) {
567
            $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('getGroups'));
568
            $result = $this->_queryDB($sql);
569
570
            if($result !== false && count($result)) {
571
                foreach($result as $row) {
572
                    $groups[] = $row['group'];
573
                }
574
            }
575
            return $groups;
576
        }
577
        return false;
578
    }
579
580
    /**
581
     * Retrieves the user id of a given user name
582
     *
583
     * The database connection must already be established
584
     * for this function to work. Otherwise it will return
585
     * false.
586
     *
587
     * @author Matthias Grimm <[email protected]>
588
     *
589
     * @param  string $user user whose id is desired
590
     * @return mixed  user id
591
     */
592
    protected function _getUserID($user) {
593
        if($this->dbcon) {
594
            $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('getUserID'));
595
            $result = $this->_queryDB($sql);
596
            return $result === false ? false : $result[0]['id'];
597
        }
598
        return false;
599
    }
600
601
    /**
602
     * Adds a new User to the database.
603
     *
604
     * The database connection must already be established
605
     * for this function to work. Otherwise it will return
606
     * false.
607
     *
608
     * @author  Andreas Gohr <[email protected]>
609
     * @author  Chris Smith <[email protected]>
610
     * @author  Matthias Grimm <[email protected]>
611
     *
612
     * @param  string $user  login of the user
613
     * @param  string $pwd   encrypted password
614
     * @param  string $name  full name of the user
615
     * @param  string $mail  email address
616
     * @param  array  $grps  array of groups the user should become member of
617
     * @return bool
618
     */
619
    protected function _addUser($user, $pwd, $name, $mail, $grps) {
620
        if($this->dbcon && is_array($grps)) {
621
            $sql = str_replace('%{user}', $this->_escape($user), $this->getConf('addUser'));
622
            $sql = str_replace('%{pass}', $this->_escape($pwd), $sql);
623
            $sql = str_replace('%{name}', $this->_escape($name), $sql);
624
            $sql = str_replace('%{email}', $this->_escape($mail), $sql);
625
            $uid = $this->_modifyDB($sql);
626
            $gid = false;
627
            $group = '';
628
629
            if($uid) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uid of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
630
                foreach($grps as $group) {
631
                    $gid = $this->_addUserToGroup($user, $group, true);
632
                    if($gid === false) break;
633
                }
634
635
                if($gid !== false){
636
                    $this->_flushUserInfoCache($user);
637
                    return true;
638
                } else {
639
                    /* remove the new user and all group relations if a group can't
640
                     * be assigned. Newly created groups will remain in the database
641
                     * and won't be removed. This might create orphaned groups but
642
                     * is not a big issue so we ignore this problem here.
643
                     */
644
                    $this->_delUser($user);
645
                    $this->_debug("MySQL err: Adding user '$user' to group '$group' failed.", -1, __LINE__, __FILE__);
646
                }
647
            }
648
        }
649
        return false;
650
    }
651
652
    /**
653
     * Deletes a given user and all his group references.
654
     *
655
     * The database connection must already be established
656
     * for this function to work. Otherwise it will return
657
     * false.
658
     *
659
     * @author Matthias Grimm <[email protected]>
660
     *
661
     * @param  string $user username of the user to be deleted
662
     * @return bool
663
     */
664
    protected function _delUser($user) {
665
        if($this->dbcon) {
666
            $uid = $this->_getUserID($user);
667
            if($uid) {
668
                $sql = str_replace('%{uid}', $this->_escape($uid), $this->getConf('delUserRefs'));
669
                $this->_modifyDB($sql);
670
                $sql = str_replace('%{uid}', $this->_escape($uid), $this->getConf('delUser'));
671
                $sql = str_replace('%{user}', $this->_escape($user), $sql);
672
                $this->_modifyDB($sql);
673
                $this->_flushUserInfoCache($user);
674
                return true;
675
            }
676
        }
677
        return false;
678
    }
679
680
    /**
681
     * Flush cached user information
682
     *
683
     * @author Christopher Smith <[email protected]>
684
     *
685
     * @param  string  $user username of the user whose data is to be removed from the cache
686
     *                       if null, empty the whole cache
687
     */
688
    protected function _flushUserInfoCache($user=null) {
689
        if (is_null($user)) {
690
            $this->cacheUserInfo = array();
691
        } else {
692
            unset($this->cacheUserInfo[$user]);
693
        }
694
    }
695
696
    /**
697
     * Quick lookup to see if a user's information has been cached
698
     *
699
     * This test does not need a database connection or read lock
700
     *
701
     * @author Christopher Smith <[email protected]>
702
     *
703
     * @param  string  $user  username to be looked up in the cache
704
     * @param  bool    $requireGroups  true, if cached info should include group memberships
705
     *
706
     * @return bool    existence of required user information in the cache
707
     */
708
    protected function _cacheExists($user, $requireGroups=true) {
709
        if (isset($this->cacheUserInfo[$user])) {
710
            if (!is_array($this->cacheUserInfo[$user])) {
711
                return true;          // user doesn't exist
712
            }
713
714
            if (!$requireGroups || isset($this->cacheUserInfo[$user]['grps'])) {
715
                return true;
716
            }
717
        }
718
719
        return false;
720
    }
721
722
    /**
723
     * Get a user's information
724
     *
725
     * The database connection must already be established for this function to work.
726
     *
727
     * @author Christopher Smith <[email protected]>
728
     *
729
     * @param  string  $user  username of the user whose information is being reterieved
730
     * @param  bool    $requireGroups  true if group memberships should be included
731
     * @param  bool    $useCache       true if ok to return cached data & to cache returned data
732
     *
733
     * @return mixed   false|array     false if the user doesn't exist
734
     *                                 array containing user information if user does exist
735
     */
736
    protected function _getUserInfo($user, $requireGroups=true, $useCache=true) {
737
        $info = null;
738
739
        if ($useCache && isset($this->cacheUserInfo[$user])) {
740
            $info = $this->cacheUserInfo[$user];
741
        }
742
743
        if (is_null($info)) {
744
            $info = $this->_retrieveUserInfo($user);
745
        }
746
747
        if (($requireGroups == true) && $info && !isset($info['grps'])) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
748
            $info['grps'] = $this->_getGroups($user);
749
        }
750
751
        if ($useCache) {
752
            $this->cacheUserInfo[$user] = $info;
753
        }
754
755
        return $info;
756
    }
757
758
    /**
759
     * retrieveUserInfo
760
     *
761
     * Gets the data for a specific user. The database connection
762
     * must already be established for this function to work.
763
     * Otherwise it will return 'false'.
764
     *
765
     * @author Matthias Grimm <[email protected]>
766
     *
767
     * @param  string $user  user's nick to get data for
768
     * @return false|array false on error, user info on success
769
     */
770
    protected function _retrieveUserInfo($user) {
771
        $sql    = str_replace('%{user}', $this->_escape($user), $this->getConf('getUserInfo'));
772
        $result = $this->_queryDB($sql);
773
        if($result !== false && count($result)) {
774
            $info         = $result[0];
775
            return $info;
776
        }
777
        return false;
778
    }
779
780
    /**
781
     * Updates the user info in the database
782
     *
783
     * Update a user data structure in the database according changes
784
     * given in an array. The user name can only be changes if it didn't
785
     * exists already. If the new user name exists the update procedure
786
     * will be aborted. The database keeps unchanged.
787
     *
788
     * The database connection has already to be established for this
789
     * function to work. Otherwise it will return 'false'.
790
     *
791
     * The password will be encrypted if necessary.
792
     *
793
     * @param  string $user    user's nick being updated
794
     * @param  array $changes  array of items to change as pairs of item and value
795
     * @return bool true on success or false on error
796
     *
797
     * @author Matthias Grimm <[email protected]>
798
     */
799
    protected function _updateUserInfo($user, $changes) {
800
        $sql = $this->getConf('updateUser')." ";
801
        $cnt = 0;
802
        $err = 0;
803
804
        if($this->dbcon) {
805
            $uid = $this->_getUserID($user);
806
            if ($uid === false) {
807
                return false;
808
            }
809
810
            foreach($changes as $item => $value) {
811
                if($item == 'user') {
812
                    if(($this->_getUserID($changes['user']))) {
813
                        $err = 1; /* new username already exists */
814
                        break; /* abort update */
815
                    }
816
                    if($cnt++ > 0) $sql .= ", ";
817
                    $sql .= str_replace('%{user}', $value, $this->getConf('UpdateLogin'));
818
                } else if($item == 'name') {
819
                    if($cnt++ > 0) $sql .= ", ";
820
                    $sql .= str_replace('%{name}', $value, $this->getConf('UpdateName'));
821
                } else if($item == 'pass') {
822
                    if(!$this->getConf('forwardClearPass'))
823
                        $value = auth_cryptPassword($value);
824
                    if($cnt++ > 0) $sql .= ", ";
825
                    $sql .= str_replace('%{pass}', $value, $this->getConf('UpdatePass'));
826
                } else if($item == 'mail') {
827
                    if($cnt++ > 0) $sql .= ", ";
828
                    $sql .= str_replace('%{email}', $value, $this->getConf('UpdateEmail'));
829
                }
830
            }
831
832
            if($err == 0) {
833
                if($cnt > 0) {
834
                    $sql .= " ".str_replace('%{uid}', $uid, $this->getConf('UpdateTarget'));
835
                    if(get_class($this) == 'auth_mysql') $sql .= " LIMIT 1"; //some PgSQL inheritance comp.
836
                    $this->_modifyDB($sql);
837
                    $this->_flushUserInfoCache($user);
838
                }
839
                return true;
840
            }
841
        }
842
        return false;
843
    }
844
845
    /**
846
     * Retrieves the group id of a given group name
847
     *
848
     * The database connection must already be established
849
     * for this function to work. Otherwise it will return
850
     * false.
851
     *
852
     * @author Matthias Grimm <[email protected]>
853
     *
854
     * @param  string $group   group name which id is desired
855
     * @return false|string group id
856
     */
857
    protected function _getGroupID($group) {
858
        if($this->dbcon) {
859
            $sql    = str_replace('%{group}', $this->_escape($group), $this->getConf('getGroupID'));
860
            $result = $this->_queryDB($sql);
861
            return $result === false ? false : $result[0]['id'];
862
        }
863
        return false;
864
    }
865
866
    /**
867
     * Opens a connection to a database and saves the handle for further
868
     * usage in the object. The successful call to this functions is
869
     * essential for most functions in this object.
870
     *
871
     * @author Matthias Grimm <[email protected]>
872
     *
873
     * @return bool
874
     */
875
    protected function _openDB() {
876
        if(!$this->dbcon) {
877
            $con = @mysql_connect($this->getConf('server'), $this->getConf('user'), conf_decodeString($this->getConf('password')));
878
            if($con) {
879
                if((mysql_select_db($this->getConf('database'), $con))) {
880
                    if((preg_match('/^(\d+)\.(\d+)\.(\d+).*/', mysql_get_server_info($con), $result)) == 1) {
881
                        $this->dbver = $result[1];
0 ignored issues
show
Documentation Bug introduced by
The property $dbver was declared of type integer, but $result[1] is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
882
                        $this->dbrev = $result[2];
0 ignored issues
show
Documentation Bug introduced by
The property $dbrev was declared of type integer, but $result[2] is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
883
                        $this->dbsub = $result[3];
0 ignored issues
show
Documentation Bug introduced by
The property $dbsub was declared of type integer, but $result[3] is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
884
                    }
885
                    $this->dbcon = $con;
886
                    if($this->getConf('charset')) {
887
                        mysql_query('SET CHARACTER SET "'.$this->getConf('charset').'"', $con);
888
                    }
889
                    return true; // connection and database successfully opened
890
                } else {
891
                    mysql_close($con);
892
                    $this->_debug("MySQL err: No access to database {$this->getConf('database')}.", -1, __LINE__, __FILE__);
893
                }
894
            } else {
895
                $this->_debug(
896
                    "MySQL err: Connection to {$this->getConf('user')}@{$this->getConf('server')} not possible.",
897
                    -1, __LINE__, __FILE__
898
                );
899
            }
900
901
            return false; // connection failed
902
        }
903
        return true; // connection already open
904
    }
905
906
    /**
907
     * Closes a database connection.
908
     *
909
     * @author Matthias Grimm <[email protected]>
910
     */
911
    protected function _closeDB() {
912
        if($this->dbcon) {
913
            mysql_close($this->dbcon);
914
            $this->dbcon = 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like 0 of type integer is incompatible with the declared type resource of property $dbcon.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
915
        }
916
    }
917
918
    /**
919
     * Sends a SQL query to the database and transforms the result into
920
     * an associative array.
921
     *
922
     * This function is only able to handle queries that returns a
923
     * table such as SELECT.
924
     *
925
     * @author Matthias Grimm <[email protected]>
926
     *
927
     * @param string $query  SQL string that contains the query
928
     * @return array|false with the result table
929
     */
930
    protected function _queryDB($query) {
931
        if($this->getConf('debug') >= 2) {
932
            msg('MySQL query: '.hsc($query), 0, __LINE__, __FILE__);
933
        }
934
935
        $resultarray = array();
936
        if($this->dbcon) {
937
            $result = @mysql_query($query, $this->dbcon);
938
            if($result) {
939
                while(($t = mysql_fetch_assoc($result)) !== false)
940
                    $resultarray[] = $t;
941
                mysql_free_result($result);
942
                return $resultarray;
943
            }
944
            $this->_debug('MySQL err: '.mysql_error($this->dbcon), -1, __LINE__, __FILE__);
945
        }
946
        return false;
947
    }
948
949
    /**
950
     * Sends a SQL query to the database
951
     *
952
     * This function is only able to handle queries that returns
953
     * either nothing or an id value such as INPUT, DELETE, UPDATE, etc.
954
     *
955
     * @author Matthias Grimm <[email protected]>
956
     *
957
     * @param string $query  SQL string that contains the query
958
     * @return int|bool insert id or 0, false on error
959
     */
960
    protected function _modifyDB($query) {
961
        if($this->getConf('debug') >= 2) {
962
            msg('MySQL query: '.hsc($query), 0, __LINE__, __FILE__);
963
        }
964
965
        if($this->dbcon) {
966
            $result = @mysql_query($query, $this->dbcon);
967
            if($result) {
968
                $rc = mysql_insert_id($this->dbcon); //give back ID on insert
969
                if($rc !== false) return $rc;
970
            }
971
            $this->_debug('MySQL err: '.mysql_error($this->dbcon), -1, __LINE__, __FILE__);
972
        }
973
        return false;
974
    }
975
976
    /**
977
     * Locked a list of tables for exclusive access so that modifications
978
     * to the database can't be disturbed by other threads. The list
979
     * could be set with $conf['plugin']['authmysql']['TablesToLock'] = array()
980
     *
981
     * If aliases for tables are used in SQL statements, also this aliases
982
     * must be locked. For eg. you use a table 'user' and the alias 'u' in
983
     * some sql queries, the array must looks like this (order is important):
984
     *   array("user", "user AS u");
985
     *
986
     * MySQL V3 is not able to handle transactions with COMMIT/ROLLBACK
987
     * so that this functionality is simulated by this function. Nevertheless
988
     * it is not as powerful as transactions, it is a good compromise in safty.
989
     *
990
     * @author Matthias Grimm <[email protected]>
991
     *
992
     * @param string $mode  could be 'READ' or 'WRITE'
993
     * @return bool
994
     */
995
    protected function _lockTables($mode) {
996
        if($this->dbcon) {
997
            $ttl = $this->getConf('TablesToLock');
998
            if(is_array($ttl) && !empty($ttl)) {
999
                if($mode == "READ" || $mode == "WRITE") {
1000
                    $sql = "LOCK TABLES ";
1001
                    $cnt = 0;
1002
                    foreach($ttl as $table) {
1003
                        if($cnt++ != 0) $sql .= ", ";
1004
                        $sql .= "$table $mode";
1005
                    }
1006
                    $this->_modifyDB($sql);
1007
                    return true;
1008
                }
1009
            }
1010
        }
1011
        return false;
1012
    }
1013
1014
    /**
1015
     * Unlock locked tables. All existing locks of this thread will be
1016
     * abrogated.
1017
     *
1018
     * @author Matthias Grimm <[email protected]>
1019
     *
1020
     * @return bool
1021
     */
1022
    protected function _unlockTables() {
1023
        if($this->dbcon) {
1024
            $this->_modifyDB("UNLOCK TABLES");
1025
            return true;
1026
        }
1027
        return false;
1028
    }
1029
1030
    /**
1031
     * Transforms the filter settings in an filter string for a SQL database
1032
     * The database connection must already be established, otherwise the
1033
     * original SQL string without filter criteria will be returned.
1034
     *
1035
     * @author Matthias Grimm <[email protected]>
1036
     *
1037
     * @param  string $sql     SQL string to which the $filter criteria should be added
1038
     * @param  array $filter  array of filter criteria as pairs of item and pattern
1039
     * @return string SQL string with attached $filter criteria on success, original SQL string on error
1040
     */
1041
    protected function _createSQLFilter($sql, $filter) {
1042
        $SQLfilter = "";
1043
        $cnt       = 0;
1044
1045
        if($this->dbcon) {
1046
            foreach($filter as $item => $pattern) {
1047
                $tmp = '%'.$this->_escape($pattern).'%';
1048
                if($item == 'user') {
1049
                    if($cnt++ > 0) $SQLfilter .= " AND ";
1050
                    $SQLfilter .= str_replace('%{user}', $tmp, $this->getConf('FilterLogin'));
1051
                } else if($item == 'name') {
1052
                    if($cnt++ > 0) $SQLfilter .= " AND ";
1053
                    $SQLfilter .= str_replace('%{name}', $tmp, $this->getConf('FilterName'));
1054
                } else if($item == 'mail') {
1055
                    if($cnt++ > 0) $SQLfilter .= " AND ";
1056
                    $SQLfilter .= str_replace('%{email}', $tmp, $this->getConf('FilterEmail'));
1057
                } else if($item == 'grps') {
1058
                    if($cnt++ > 0) $SQLfilter .= " AND ";
1059
                    $SQLfilter .= str_replace('%{group}', $tmp, $this->getConf('FilterGroup'));
1060
                }
1061
            }
1062
1063
            // we have to check SQLfilter here and must not use $cnt because if
1064
            // any of cnf['Filter????'] is not defined, a malformed SQL string
1065
            // would be generated.
1066
1067
            if(strlen($SQLfilter)) {
1068
                $glue = strpos(strtolower($sql), "where") ? " AND " : " WHERE ";
1069
                $sql  = $sql.$glue.$SQLfilter;
1070
            }
1071
        }
1072
1073
        return $sql;
1074
    }
1075
1076
    /**
1077
     * Escape a string for insertion into the database
1078
     *
1079
     * @author Andreas Gohr <[email protected]>
1080
     *
1081
     * @param  string  $string The string to escape
1082
     * @param  boolean $like   Escape wildcard chars as well?
1083
     * @return string
1084
     */
1085
    protected function _escape($string, $like = false) {
1086
        if($this->dbcon) {
1087
            $string = mysql_real_escape_string($string, $this->dbcon);
1088
        } else {
1089
            $string = addslashes($string);
1090
        }
1091
        if($like) {
1092
            $string = addcslashes($string, '%_');
1093
        }
1094
        return $string;
1095
    }
1096
1097
    /**
1098
     * Wrapper around msg() but outputs only when debug is enabled
1099
     *
1100
     * @param string $message
1101
     * @param int    $err
1102
     * @param int    $line
1103
     * @param string $file
1104
     * @return void
1105
     */
1106
    protected function _debug($message, $err, $line, $file) {
1107
        if(!$this->getConf('debug')) return;
1108
        msg($message, $err, $line, $file);
1109
    }
1110
}
1111