Passed
Pull Request — master (#1560)
by Michael
08:52
created

XoopsMemberHandler::getUsersByGroupLink()   F

Complexity

Conditions 46
Paths > 20000

Size

Total Lines 244
Code Lines 139

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 46
eloc 139
c 5
b 0
f 0
nc 4976640
nop 4
dl 0
loc 244
rs 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * XOOPS Kernel Class
4
 *
5
 * You may not change or alter any portion of this comment or credits
6
 * of supporting developers from this source code or any supporting source code
7
 * which is considered copyrighted (c) material of the original comment or credit authors.
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
 *
12
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
13
 * @license             GNU GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html)
14
 * @package             kernel
15
 * @since               2.0.0
16
 * @author              Kazumi Ono (AKA onokazu) http://www.myweb.ne.jp/, http://jp.xoops.org/
17
 */
18
defined('XOOPS_ROOT_PATH') || exit('Restricted access');
19
20
require_once __DIR__ . '/user.php';
21
require_once __DIR__ . '/group.php';
22
23
/**
24
 * XOOPS member handler class.
25
 * This class provides simple interface (a facade class) for handling groups/users/
26
 * membership data.
27
 *
28
 *
29
 * @author              Kazumi Ono <[email protected]>
30
 * @copyright       (c) 2000-2025 XOOPS Project (https://xoops.org)
31
 * @package             kernel
32
 */
33
class XoopsMemberHandler
34
{
35
    /**
36
     * holds reference to group handler(DAO) class
37
     * @access private
38
     */
39
    protected $groupHandler;
40
41
    /**
42
     * holds reference to user handler(DAO) class
43
     */
44
    protected $userHandler;
45
46
    /**
47
     * holds reference to membership handler(DAO) class
48
     */
49
    protected $membershipHandler;
50
51
    /**
52
     * holds temporary user objects
53
     */
54
    protected $membersWorkingList = [];
55
56
    /**
57
     * constructor
58
     * @param XoopsDatabase|null| $db
59
     */
60
    public function __construct(XoopsDatabase $db)
61
    {
62
        $this->groupHandler = new XoopsGroupHandler($db);
63
        $this->userHandler = new XoopsUserHandler($db);
64
        $this->membershipHandler = new XoopsMembershipHandler($db);
65
    }
66
67
    /**
68
     * create a new group
69
     *
70
     * @return XoopsGroup XoopsGroup reference to the new group
71
     */
72
    public function &createGroup()
73
    {
74
        $inst = $this->groupHandler->create();
75
76
        return $inst;
77
    }
78
79
    /**
80
     * create a new user
81
     *
82
     * @return XoopsUser reference to the new user
83
     */
84
    public function createUser()
85
    {
86
        $inst = $this->userHandler->create();
87
88
        return $inst;
89
    }
90
91
    /**
92
     * retrieve a group
93
     *
94
     * @param  int $id ID for the group
95
     * @return XoopsGroup|false XoopsGroup reference to the group
96
     */
97
    public function getGroup($id)
98
    {
99
        return $this->groupHandler->get($id);
100
    }
101
102
    /**
103
     * retrieve a user
104
     *
105
     * @param  int $id ID for the user
106
     * @return XoopsUser reference to the user
107
     */
108
    public function getUser($id)
109
    {
110
        if (!isset($this->membersWorkingList[$id])) {
111
            $this->membersWorkingList[$id] = $this->userHandler->get($id);
112
        }
113
114
        return $this->membersWorkingList[$id];
115
    }
116
117
    /**
118
     * delete a group
119
     *
120
     * @param  XoopsGroup $group reference to the group to delete
121
     * @return bool   FALSE if failed
122
     */
123
    public function deleteGroup(XoopsGroup $group)
124
    {
125
        $s1 = $this->membershipHandler->deleteAll(new Criteria('groupid', $group->getVar('groupid')));
0 ignored issues
show
Bug introduced by
It seems like $group->getVar('groupid') can also be of type array and array; however, parameter $value of Criteria::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

125
        $s1 = $this->membershipHandler->deleteAll(new Criteria('groupid', /** @scrutinizer ignore-type */ $group->getVar('groupid')));
Loading history...
126
        $s2 = $this->groupHandler->delete($group);
127
128
        return ($s1 && $s2);// ? true : false;
129
    }
130
131
    /**
132
     * delete a user
133
     *
134
     * @param  XoopsUser $user reference to the user to delete
135
     * @return bool   FALSE if failed
136
     */
137
    public function deleteUser(XoopsUser $user)
138
    {
139
        $s1 = $this->membershipHandler->deleteAll(new Criteria('uid', $user->getVar('uid')));
0 ignored issues
show
Bug introduced by
It seems like $user->getVar('uid') can also be of type array and array; however, parameter $value of Criteria::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

139
        $s1 = $this->membershipHandler->deleteAll(new Criteria('uid', /** @scrutinizer ignore-type */ $user->getVar('uid')));
Loading history...
140
        $s2 = $this->userHandler->delete($user);
141
142
        return ($s1 && $s2);// ? true : false;
143
    }
144
145
    /**
146
     * insert a group into the database
147
     *
148
     * @param  XoopsGroup $group reference to the group to insert
149
     * @return bool       TRUE if already in database and unchanged
150
     *                           FALSE on failure
151
     */
152
    public function insertGroup(XoopsGroup $group)
153
    {
154
        return $this->groupHandler->insert($group);
155
    }
156
157
    /**
158
     * insert a user into the database
159
     *
160
     * @param XoopsUser $user reference to the user to insert
161
     * @param bool      $force
162
     *
163
     * @return bool TRUE if already in database and unchanged
164
     *              FALSE on failure
165
     */
166
    public function insertUser(XoopsUser $user, $force = false)
167
    {
168
        return $this->userHandler->insert($user, $force);
169
    }
170
171
    /**
172
     * retrieve groups from the database
173
     *
174
     * @param  CriteriaElement $criteria  {@link CriteriaElement}
175
     * @param  bool            $id_as_key use the group's ID as key for the array?
176
     * @return array           array of {@link XoopsGroup} objects
177
     */
178
    public function getGroups(?CriteriaElement $criteria = null, $id_as_key = false)
179
    {
180
        return $this->groupHandler->getObjects($criteria, $id_as_key);
181
    }
182
183
    /**
184
     * retrieve users from the database
185
     *
186
     * @param  CriteriaElement $criteria  {@link CriteriaElement}
187
     * @param  bool            $id_as_key use the group's ID as key for the array?
188
     * @return array           array of {@link XoopsUser} objects
189
     */
190
    public function getUsers(?CriteriaElement $criteria = null, $id_as_key = false)
191
    {
192
        return $this->userHandler->getObjects($criteria, $id_as_key);
193
    }
194
195
    /**
196
     * get a list of groupnames and their IDs
197
     *
198
     * @param  CriteriaElement $criteria {@link CriteriaElement} object
199
     * @return array           associative array of group-IDs and names
200
     */
201
    public function getGroupList(?CriteriaElement $criteria = null)
202
    {
203
        $groups = $this->groupHandler->getObjects($criteria, true);
204
        $ret    = [];
205
        foreach (array_keys($groups) as $i) {
206
            $ret[$i] = $groups[$i]->getVar('name');
207
        }
208
209
        return $ret;
210
    }
211
212
    /**
213
     * get a list of usernames and their IDs
214
     *
215
     * @param  CriteriaElement $criteria {@link CriteriaElement} object
216
     * @return array           associative array of user-IDs and names
217
     */
218
    public function getUserList(?CriteriaElement $criteria = null)
219
    {
220
        $users = & $this->userHandler->getObjects($criteria, true);
221
        $ret   = [];
222
        foreach (array_keys($users) as $i) {
223
            $ret[$i] = $users[$i]->getVar('uname');
224
        }
225
226
        return $ret;
227
    }
228
229
    /**
230
     * add a user to a group
231
     *
232
     * @param  int $group_id ID of the group
233
     * @param  int $user_id  ID of the user
234
     * @return XoopsMembership XoopsMembership
235
     */
236
    public function addUserToGroup($group_id, $user_id)
237
    {
238
        $mship = $this->membershipHandler->create();
239
        $mship->setVar('groupid', $group_id);
240
        $mship->setVar('uid', $user_id);
241
242
        return $this->membershipHandler->insert($mship);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->membershipHandler->insert($mship) returns the type boolean which is incompatible with the documented return type XoopsMembership.
Loading history...
243
    }
244
245
    /**
246
     * remove a list of users from a group
247
     *
248
     * @param  int   $group_id ID of the group
249
     * @param  array $user_ids array of user-IDs
250
     * @return bool  success?
251
     */
252
    public function removeUsersFromGroup($group_id, $user_ids = [])
253
    {
254
        $criteria = new CriteriaCompo();
255
        $criteria->add(new Criteria('groupid', $group_id));
256
        $criteria2 = new CriteriaCompo();
257
        foreach ($user_ids as $uid) {
258
            $criteria2->add(new Criteria('uid', $uid), 'OR');
259
        }
260
        $criteria->add($criteria2);
261
262
        return $this->membershipHandler->deleteAll($criteria);
263
    }
264
265
    /**
266
     * get a list of users belonging to a group
267
     *
268
     * @param  int  $group_id ID of the group
269
     * @param  bool $asobject return the users as objects?
270
     * @param  int  $limit    number of users to return
271
     * @param  int  $start    index of the first user to return
272
     * @return array Array of {@link XoopsUser} objects (if $asobject is TRUE)
273
     *                        or of associative arrays matching the record structure in the database.
274
     */
275
    public function getUsersByGroup($group_id, $asobject = false, $limit = 0, $start = 0)
276
    {
277
        $user_ids = $this->membershipHandler->getUsersByGroup($group_id, $limit, $start);
278
        if (!$asobject) {
279
            return $user_ids;
280
        } else {
281
            $ret = [];
282
            foreach ($user_ids as $u_id) {
283
                $user = $this->getUser($u_id);
284
                if (is_object($user)) {
285
                    $ret[] = &$user;
286
                }
287
                unset($user);
288
            }
289
290
            return $ret;
291
        }
292
    }
293
294
    /**
295
     * get a list of groups that a user is member of
296
     *
297
     * @param  int  $user_id  ID of the user
298
     * @param  bool $asobject return groups as {@link XoopsGroup} objects or arrays?
299
     * @return array array of objects or arrays
300
     */
301
    public function getGroupsByUser($user_id, $asobject = false)
302
    {
303
        $group_ids = $this->membershipHandler->getGroupsByUser($user_id);
304
        if (!$asobject) {
305
            return $group_ids;
306
        } else {
307
            $ret = [];
308
            foreach ($group_ids as $g_id) {
309
                $ret[] = $this->getGroup($g_id);
310
            }
311
312
            return $ret;
313
        }
314
    }
315
316
    /**
317
     * log in a user
318
     *
319
     * @param  string    $uname username as entered in the login form
320
     * @param  string    $pwd   password entered in the login form
321
     *
322
     * @return XoopsUser|false logged in XoopsUser, FALSE if failed to log in
323
     */
324
    public function loginUser($uname, $pwd)
325
    {
326
        $db = XoopsDatabaseFactory::getDatabaseConnection();
327
        $uname = $db->escape($uname);
0 ignored issues
show
Bug introduced by
The method escape() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

327
        /** @scrutinizer ignore-call */ 
328
        $uname = $db->escape($uname);
Loading history...
328
        $pwd = $db->escape($pwd);
329
        $criteria = new Criteria('uname', $uname);
330
        $user = & $this->userHandler->getObjects($criteria, false);
331
        if (!$user || count($user) != 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
332
            return false;
333
        }
334
335
        $hash = $user[0]->pass();
336
        $type = substr($user[0]->pass(), 0, 1);
337
        // see if we have a crypt like signature, old md5 hash is just hex digits
338
        if ($type === '$') {
339
            if (!password_verify($pwd, $hash)) {
340
                return false;
341
            }
342
            // check if hash uses the best algorithm (i.e. after a PHP upgrade)
343
            $rehash = password_needs_rehash($hash, PASSWORD_DEFAULT);
344
        } else {
345
            if ($hash != md5($pwd)) {
346
                return false;
347
            }
348
            $rehash = true; // automatically update old style
349
        }
350
        // hash used an old algorithm, so make it stronger
351
        if ($rehash) {
352
            if ($this->getColumnCharacterLength('users', 'pass') < 255) {
353
                error_log('Upgrade required on users table!');
354
            } else {
355
                $user[0]->setVar('pass', password_hash($pwd, PASSWORD_DEFAULT));
356
                $this->userHandler->insert($user[0]);
357
            }
358
        }
359
        return $user[0];
360
    }
361
362
    /**
363
     * Get maximum character length for a table column
364
     *
365
     * @param string $table  database table
366
     * @param string $column table column
367
     *
368
     * @return int|null max length or null on error
369
     */
370
    public function getColumnCharacterLength($table, $column)
371
    {
372
        /** @var XoopsMySQLDatabase $db */
373
        $db = XoopsDatabaseFactory::getDatabaseConnection();
374
375
        $dbname = constant('XOOPS_DB_NAME');
376
        $table = $db->prefix($table);
377
378
        $sql = sprintf(
379
            'SELECT `CHARACTER_MAXIMUM_LENGTH` FROM `information_schema`.`COLUMNS` '
380
            . "WHERE TABLE_SCHEMA = '%s'AND TABLE_NAME = '%s' AND COLUMN_NAME = '%s'",
381
            $db->escape($dbname),
382
            $db->escape($table),
383
            $db->escape($column),
384
        );
385
386
        /** @var mysqli_result $result */
387
        $result = $db->query($sql);
388
        if ($db->isResultSet($result)) {
389
            $row = $db->fetchRow($result);
390
            if ($row) {
391
                $columnLength = $row[0];
392
                return (int) $columnLength;
393
            }
394
        }
395
        return null;
396
    }
397
398
    /**
399
     * count users matching certain conditions
400
     *
401
     * @param  CriteriaElement $criteria {@link CriteriaElement} object
402
     * @return int
403
     */
404
    public function getUserCount(?CriteriaElement $criteria = null)
405
    {
406
        return $this->userHandler->getCount($criteria);
407
    }
408
409
    /**
410
     * count users belonging to a group
411
     *
412
     * @param  int $group_id ID of the group
413
     * @return int
414
     */
415
    public function getUserCountByGroup($group_id)
416
    {
417
        return $this->membershipHandler->getCount(new Criteria('groupid', $group_id));
418
    }
419
420
    /**
421
     * updates a single field in a users record
422
     *
423
     * @param  XoopsUser $user       reference to the {@link XoopsUser} object
424
     * @param  string    $fieldName  name of the field to update
425
     * @param  string    $fieldValue updated value for the field
426
     * @return bool      TRUE if success or unchanged, FALSE on failure
427
     */
428
    public function updateUserByField(XoopsUser $user, $fieldName, $fieldValue)
429
    {
430
        $user->setVar($fieldName, $fieldValue);
431
432
        return $this->insertUser($user);
433
    }
434
435
    /**
436
     * updates a single field in a users record
437
     *
438
     * @param  string          $fieldName  name of the field to update
439
     * @param  string          $fieldValue updated value for the field
440
     * @param  CriteriaElement $criteria   {@link CriteriaElement} object
441
     * @return bool            TRUE if success or unchanged, FALSE on failure
442
     */
443
    public function updateUsersByField($fieldName, $fieldValue, ?CriteriaElement $criteria = null)
444
    {
445
        return $this->userHandler->updateAll($fieldName, $fieldValue, $criteria);
446
    }
447
448
    /**
449
     * activate a user
450
     *
451
     * @param  XoopsUser $user reference to the {@link XoopsUser} object
452
     * @return mixed      successful? false on failure
453
     */
454
    public function activateUser(XoopsUser $user)
455
    {
456
        if ($user->getVar('level') != 0) {
457
            return true;
458
        }
459
        $user->setVar('level', 1);
460
        $actkey = substr(md5(uniqid(mt_rand(), 1)), 0, 8);
461
        $user->setVar('actkey', $actkey);
462
463
        return $this->userHandler->insert($user, true);
464
    }
465
466
    protected function allowedSortMap()
467
    {
468
        // Maps both prefixed and non-prefixed column names for flexibility
469
        // This allows sorting by 'uid' or 'u.uid' while maintaining security
470
        return [
471
            'uid'            => 'u.uid',
472
            'uname'          => 'u.uname',
473
            'email'          => 'u.email',
474
            'user_regdate'   => 'u.user_regdate',
475
            'last_login'     => 'u.last_login',
476
            'user_avatar'    => 'u.user_avatar',
477
            'name'           => 'u.name',
478
            // Prefixed versions for explicit table references
479
            'u.uid'          => 'u.uid',
480
            'u.uname'        => 'u.uname',
481
            'u.email'        => 'u.email',
482
            'u.user_regdate' => 'u.user_regdate',
483
            'u.last_login'   => 'u.last_login',
484
            'u.user_avatar'  => 'u.user_avatar',
485
            'u.name'         => 'u.name',
486
        ];
487
    }
488
489
490
    /**
491
     * Get a list of users belonging to certain groups and matching criteria
492
     * Temporary solution
493
     *
494
     * @param  array           $groups    IDs of groups
495
     * @param  CriteriaElement $criteria  {@link CriteriaElement} object
496
     * @param  bool            $asobject  return the users as objects?
497
     * @param  bool            $id_as_key use the UID as key for the array if $asobject is TRUE
498
     * @return array           Array of {@link XoopsUser} objects (if $asobject is TRUE)
499
     *                                    or of associative arrays matching the record structure in the database.
500
     */
501
502
    public function getUsersByGroupLink(
503
        $groups,
504
        $criteria = null,
505
        $asobject = false,
506
        $id_as_key = false
507
    ) {
508
        // Type coercion for backwards compatibility
509
        $groups = is_array($groups) ? $groups : [$groups];
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
510
        $asobject = (bool)$asobject;
511
        $id_as_key = (bool)$id_as_key;
512
513
        // Debug configuration using only current XOOPS debug system
514
        // Check XOOPS debug mode - we only want PHP debugging (1=inline, 2=popup)
515
        $xoopsDebugMode = isset($GLOBALS['xoopsConfig']['debug_mode']) ? (int)$GLOBALS['xoopsConfig']['debug_mode'] : 0;
516
        $xoopsPhpDebugEnabled = ($xoopsDebugMode === 1 || $xoopsDebugMode === 2);
517
518
        // Check if debug is allowed for current user based on debugLevel
519
        $xoopsDebugAllowed = $xoopsPhpDebugEnabled;
520
        if ($xoopsPhpDebugEnabled && isset($GLOBALS['xoopsConfig']['debugLevel'])) {
521
            $debugLevel = (int)$GLOBALS['xoopsConfig']['debugLevel'];
522
            $xoopsUser = $GLOBALS['xoopsUser'] ?? null;
523
            $xoopsUserIsAdmin = isset($GLOBALS['xoopsUserIsAdmin']) ? $GLOBALS['xoopsUserIsAdmin'] : false;
524
525
            // Apply XOOPS debug level restrictions
526
            switch ($debugLevel) {
527
                case 2: // Admins only
528
                    $xoopsDebugAllowed = $xoopsUserIsAdmin;
529
                    break;
530
                case 1: // Members only
531
                    $xoopsDebugAllowed = ($xoopsUser !== null);
532
                    break;
533
                case 0: // All users
534
                default:
535
                    $xoopsDebugAllowed = true;
536
                    break;
537
            }
538
        }
539
540
        // Production safety check - use secure environment detection
541
        // Note: SERVER_NAME can be spoofed via Host header, so it's not secure for production detection
542
        // For security, set XOOPS_ENV=production in your server environment or use a config constant
543
        $isProd = false;
544
545
        if (defined('XOOPS_PRODUCTION') && XOOPS_PRODUCTION) {
0 ignored issues
show
Bug introduced by
The constant XOOPS_PRODUCTION was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
546
            // Most secure: use a defined constant set in configuration
547
            $isProd = true;
548
        } elseif (getenv('XOOPS_ENV') === 'production') {
549
            // Secure: use environment variable (not spoofable by clients)
550
            $isProd = true;
551
        } else {
552
            // Fallback: assume production unless explicitly in known development environments
553
            // This is more secure than the old approach - defaults to restrictive mode
554
            $isProd = true;
555
            // Only allow debug in explicitly known safe development indicators
556
            if ((defined('XOOPS_DEBUG') && XOOPS_DEBUG)
0 ignored issues
show
Bug introduced by
The constant XOOPS_DEBUG was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
557
                || (php_sapi_name() === 'cli')
558
                || (isset($_SERVER['SERVER_ADDR']) && $_SERVER['SERVER_ADDR'] === '127.0.0.1')) {
559
                $isProd = false;
560
            }
561
        }
562
563
        // Enable SQL logging only if XOOPS PHP debug is allowed and not in production
564
        $isDebug = $xoopsDebugAllowed && !$isProd;
565
566
        /**
567
         * Redact sensitive SQL literals in debug logs while preserving query structure
568
         * @param string $sql The SQL query to redact
569
         * @return string Redacted SQL query
570
         */
571
        $redactSql = static function (string $sql): string {
572
            // Replace quoted strings with placeholders
573
            $sql = preg_replace("/'[^']*'/", "'?'", $sql);
574
            $sql = preg_replace('/"[^"]*"/', '"?"', $sql);
575
            // Replace hex literals
576
            $sql = preg_replace("/x'[0-9A-Fa-f]+'/", "x'?'", $sql);
577
            // Replace large numbers (potential IDs) but keep small ones
578
            $sql = preg_replace('/\b\d{6,}\b/', '[ID]', $sql);
579
            return $sql;
580
        };
581
582
        $ret           = [];
583
        $criteriaCompo = new CriteriaCompo();
584
        $select        = $asobject ? 'u.*' : 'u.uid';
585
        $sql = "SELECT {$select} FROM " . $this->userHandler->db->prefix('users') . ' u';
586
        $whereParts = [];
587
        $limit = 0;
588
        $start = 0;
589
590
        // Sanitize and validate groups once - clean and efficient
591
        $validGroups = array_values(
592
            array_unique(
593
                array_filter(
594
            array_map('intval', $groups),
595
                static fn($id) => $id > 0
596
                )
597
            )
598
        );
599
600
        // Build group filtering with EXISTS subquery (no re-validation needed)
601
        if (!empty($validGroups)) {
602
            $group_in = '(' . implode(', ', $validGroups) . ')';
603
            $whereParts[] = 'EXISTS (SELECT 1 FROM ' . $this->membershipHandler->db->prefix('groups_users_link')
604
                            . " m WHERE m.uid = u.uid AND m.groupid IN {$group_in})";
605
        }
606
607
        // Handle criteria - compatible with CriteriaElement and subclasses
608
        if ($criteria instanceof \CriteriaElement) {
609
            $criteriaCompo->add($criteria, 'AND');
610
            $sqlCriteria = trim($criteriaCompo->render());
611
612
            // Remove WHERE keyword if present
613
            $sqlCriteria = preg_replace('/^\s*WHERE\s+/i', '', $sqlCriteria ?? '');
614
615
            if ('' !== $sqlCriteria) {
616
                $whereParts[] = $sqlCriteria;
617
        }
618
619
            $limit = (int)$criteria->getLimit();
620
            $start = (int)$criteria->getStart();
621
        }
622
623
        // Build WHERE clause
624
        if (!empty($whereParts)) {
625
            $sql .= ' WHERE ' . implode(' AND ', $whereParts);
626
        }
627
628
        // Handle ORDER BY with enhanced security whitelist
629
        if ($criteria instanceof \CriteriaElement) {
630
            $sort = trim($criteria->getSort());
631
            $order = trim($criteria->getOrder());
632
            if ('' !== $sort) {
633
                // Use the whitelist method for safe sorting columns
634
                $allowedSorts = $this->allowedSortMap();
635
636
                if (isset($allowedSorts[$sort])) {
637
                    $orderDirection = ('DESC' === strtoupper($order)) ? ' DESC' : ' ASC';
638
                    $sql .= ' ORDER BY ' . $allowedSorts[$sort] . $orderDirection;
639
                }
640
            }
641
        }
642
643
        // Execute query with comprehensive error handling
644
        $result = $this->userHandler->db->query($sql, $limit, $start);
0 ignored issues
show
Bug introduced by
The method query() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

644
        /** @scrutinizer ignore-call */ 
645
        $result = $this->userHandler->db->query($sql, $limit, $start);
Loading history...
645
646
        if (!$this->userHandler->db->isResultSet($result)) {
647
            // Enhanced error logging with security considerations
648
            $logger = class_exists('XoopsLogger') ? \XoopsLogger::getInstance() : null;
649
                $error = $this->userHandler->db->error();
0 ignored issues
show
Bug introduced by
The method error() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

649
                /** @scrutinizer ignore-call */ 
650
                $error = $this->userHandler->db->error();
Loading history...
650
651
            $msg = "Database query failed in " . __METHOD__ . ": {$error}";
652
653
            if ($isDebug) {
654
                // Comprehensive log sanitizers to prevent injection and spoofing attacks
655
                $sanitizeLogValue = static function ($value): string {
656
                    $s = (string)$value;
657
                    // Strip ASCII control chars (including CR/LF) and DEL
658
                    $s = preg_replace('/[\x00-\x1F\x7F]/', '', $s);
659
                    // Strip Unicode bidi/isolation controls that can spoof log layout
660
                    // U+202A..U+202E (LRE..RLO) and U+2066..U+2069 (LRI..PDI)
661
                    $s = preg_replace('/[\x{202A}-\x{202E}\x{2066}-\x{2069}]/u', '', $s);
662
                    // Collapse excessive whitespace
663
                    $s = preg_replace('/\s+/', ' ', $s);
664
                    // Length cap with mbstring fallback
665
                    if (function_exists('mb_substr')) {
666
                        $s = mb_substr($s, 0, 256, 'UTF-8');
667
                    } else {
668
                        $s = substr($s, 0, 256);
669
                    }
670
                    return $s;
671
                };
672
673
                $sanitizeMethod = static function ($method) use ($sanitizeLogValue): string {
674
                    $m     = strtoupper($sanitizeLogValue($method));
675
                    $allow = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
676
                    return in_array($m, $allow, true) ? $m : 'OTHER';
677
                };
678
679
                $sanitizeUri = static function ($uri) use ($sanitizeLogValue): string {
680
                    $u     = (string)$uri;
681
                    $parts = parse_url($u);
682
                    $path  = $sanitizeLogValue($parts['path'] ?? '/');
683
                    // Redact sensitive query params
684
                    $qs = '';
685
                    if (!empty($parts['query'])) {
686
                        parse_str($parts['query'], $q);
687
                        $redact = ['token', 'access_token', 'id_token', 'password', 'pass', 'pwd', 'secret', 'key', 'api_key', 'apikey', 'auth', 'authorization', 'session', 'sid', 'code'];
688
                        foreach ($q as $k => &$v) {
689
                            $kLower = strtolower((string)$k);
690
                            if (in_array($kLower, $redact, true)) {
691
                                $v = 'REDACTED';
692
                            } else {
693
                                $v = is_array($v) ? $sanitizeLogValue(json_encode($v)) : $sanitizeLogValue($v);
694
                            }
695
                        }
696
                        unset($v);
697
                        $qs = $sanitizeLogValue(http_build_query($q));
698
                    }
699
                    return $qs !== '' ? $path . '?' . $qs : $path;
700
                };
701
702
                // Add correlation context for easier debugging
703
                $context = [
704
                    'user_id' => isset($GLOBALS['xoopsUser']) && $GLOBALS['xoopsUser']
705
                        ? (int)$GLOBALS['xoopsUser']->getVar('uid')
706
                        : 'anonymous',
707
                    'uri'          => isset($_SERVER['REQUEST_URI'])
708
                        ? $sanitizeUri($_SERVER['REQUEST_URI'])
709
                        : 'cli',
710
                    'method'       => isset($_SERVER['REQUEST_METHOD'])
711
                        ? $sanitizeMethod($_SERVER['REQUEST_METHOD'])
712
                        : 'CLI',
713
                    'groups_count' => (int)count($validGroups),
714
                ];
715
                $msg .= ' Context: ' . json_encode($context, JSON_UNESCAPED_SLASHES);
716
                $msg .= ' SQL: ' . $redactSql($sql);
717
            }
718
719
            if ($logger) {
720
                $logger->handleError(E_USER_WARNING, $msg, __FILE__, __LINE__);
721
            } else {
722
                // Enhanced fallback logging with file/line info
723
                error_log($msg . " in " . __FILE__ . " on line " . __LINE__);
724
            }
725
726
            return $ret;
727
        }
728
729
        // Process results with enhanced type safety
730
        while (false !== ($myrow = $this->userHandler->db->fetchArray($result))) {
0 ignored issues
show
Bug introduced by
The method fetchArray() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

730
        while (false !== ($myrow = $this->userHandler->db->/** @scrutinizer ignore-call */ fetchArray($result))) {
Loading history...
731
            if ($asobject) {
732
                $user = new XoopsUser();
733
                $user->assignVars($myrow);
734
                if ($id_as_key) {
735
                    $ret[(int)$myrow['uid']] = $user;
736
                } else {
737
                    $ret[] = $user;
738
                }
739
            } else {
740
                // Ensure consistent integer return for UIDs
741
                $ret[] = (int)$myrow['uid'];
742
            }
743
        }
744
745
        return $ret;
746
    }
747
748
    /**
749
     * Get count of users belonging to certain groups and matching criteria
750
     * Temporary solution
751
     *
752
     * @param  array           $groups IDs of groups
753
     * @param  CriteriaElement $criteria
754
     * @return int             count of users
755
     */
756
    public function getUserCountByGroupLink(array $groups, ?CriteriaElement $criteria = null)
757
    {
758
        $ret           = 0;
759
        $criteriaCompo = new CriteriaCompo();
760
        $sql = "SELECT COUNT(*) FROM " . $this->userHandler->db->prefix('users') . " u WHERE ";
761
        if (!empty($groups)) {
762
            $group_in = is_array($groups) ? '(' . implode(', ', $groups) . ')' : (array) $groups;
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
763
            $sql .= " EXISTS (SELECT * FROM " . $this->membershipHandler->db->prefix('groups_users_link')
764
                . " m " . "WHERE m.groupid IN {$group_in} and m.uid = u.uid) ";
765
        }
766
767
        if (isset($criteria) && is_subclass_of($criteria, 'CriteriaElement')) {
768
            $criteriaCompo->add($criteria, 'AND');
769
        }
770
        $sql_criteria = $criteriaCompo->render();
771
772
        if ($sql_criteria) {
773
            $sql .= ' AND ' . $sql_criteria;
774
        }
775
        $result = $this->userHandler->db->query($sql);
776
        if (!$this->userHandler->db->isResultSet($result)) {
777
            return $ret;
778
        }
779
        [$ret] = $this->userHandler->db->fetchRow($result);
0 ignored issues
show
Bug introduced by
The method fetchRow() does not exist on XoopsDatabase. Since it exists in all sub-types, consider adding an abstract or default implementation to XoopsDatabase. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

779
        /** @scrutinizer ignore-call */ 
780
        [$ret] = $this->userHandler->db->fetchRow($result);
Loading history...
780
781
        return (int) $ret;
782
    }
783
}
784