Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Class used to fetch participants based on a filterset.
19
 *
20
 * @package    core_user
21
 * @copyright  2020 Michael Hawkins <michaelh@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_user\table;
26
 
27
use context;
28
use context_helper;
29
use core_table\local\filter\filterset;
30
use core_user;
31
use moodle_recordset;
32
use stdClass;
33
use core_user\fields;
34
 
35
defined('MOODLE_INTERNAL') || die;
36
 
37
require_once($CFG->dirroot . '/user/lib.php');
38
 
39
/**
40
 * Class used to fetch participants based on a filterset.
41
 *
42
 * @package    core_user
43
 * @copyright  2020 Michael Hawkins <michaelh@moodle.com>
44
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45
 */
46
class participants_search {
47
 
48
    /**
49
     * @var filterset $filterset The filterset describing which participants to include in the search.
50
     */
51
    protected $filterset;
52
 
53
    /**
54
     * @var stdClass $course The course being searched.
55
     */
56
    protected $course;
57
 
58
    /**
59
     * @var context_course $context The course context being searched.
60
     */
61
    protected $context;
62
 
63
    /**
64
     * @var string[] $userfields Names of any extra user fields to be shown when listing users.
65
     */
66
    protected $userfields;
67
 
68
    /**
69
     * Class constructor.
70
     *
71
     * @param stdClass $course The course being searched.
72
     * @param context $context The context of the search.
73
     * @param filterset $filterset The filterset used to filter the participants in a course.
74
     */
75
    public function __construct(stdClass $course, context $context, filterset $filterset) {
76
        $this->course = $course;
77
        $this->context = $context;
78
        $this->filterset = $filterset;
79
 
80
        $this->userfields = fields::get_identity_fields($this->context);
81
    }
82
 
83
    /**
84
     * Fetch participants matching the filterset.
85
     *
86
     * @param string $additionalwhere Any additional SQL to add to where.
87
     * @param array $additionalparams The additional params used by $additionalwhere.
88
     * @param string $sort Optional SQL sort.
89
     * @param int $limitfrom Return a subset of records, starting at this point (optional).
90
     * @param int $limitnum Return a subset comprising this many records (optional, required if $limitfrom is set).
91
     * @return moodle_recordset
92
     */
93
    public function get_participants(string $additionalwhere = '', array $additionalparams = [], string $sort = '',
94
            int $limitfrom = 0, int $limitnum = 0): moodle_recordset {
95
        global $DB;
96
 
97
        [
98
            'subqueryalias' => $subqueryalias,
99
            'outerselect' => $outerselect,
100
            'innerselect' => $innerselect,
101
            'outerjoins' => $outerjoins,
102
            'innerjoins' => $innerjoins,
103
            'outerwhere' => $outerwhere,
104
            'innerwhere' => $innerwhere,
105
            'params' => $params,
106
        ] = $this->get_participants_sql($additionalwhere, $additionalparams);
107
 
108
        $sql = "{$outerselect}
109
                          FROM ({$innerselect}
110
                                          FROM {$innerjoins}
111
                                 {$innerwhere}
112
                               ) {$subqueryalias}
113
                 {$outerjoins}
114
                 {$outerwhere}
115
                       {$sort}";
116
 
117
        return $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
118
    }
119
 
120
    /**
121
     * Returns the total number of participants for a given course.
122
     *
123
     * @param string $additionalwhere Any additional SQL to add to where.
124
     * @param array $additionalparams The additional params used by $additionalwhere.
125
     * @return int
126
     */
127
    public function get_total_participants_count(string $additionalwhere = '', array $additionalparams = []): int {
128
        global $DB;
129
 
130
        [
131
            'subqueryalias' => $subqueryalias,
132
            'innerselect' => $innerselect,
133
            'outerjoins' => $outerjoins,
134
            'innerjoins' => $innerjoins,
135
            'outerwhere' => $outerwhere,
136
            'innerwhere' => $innerwhere,
137
            'params' => $params,
138
        ] = $this->get_participants_sql($additionalwhere, $additionalparams);
139
 
140
        $sql = "SELECT COUNT(u.id)
141
                  FROM ({$innerselect}
142
                                  FROM {$innerjoins}
143
                         {$innerwhere}
144
                       ) {$subqueryalias}
145
         {$outerjoins}
146
         {$outerwhere}";
147
 
148
        return $DB->count_records_sql($sql, $params);
149
    }
150
 
151
    /**
152
     * Generate the SQL used to fetch filtered data for the participants table.
153
     *
154
     * @param string $additionalwhere Any additional SQL to add to where
155
     * @param array $additionalparams The additional params
156
     * @return array
157
     */
158
    protected function get_participants_sql(string $additionalwhere, array $additionalparams): array {
159
        global $CFG;
160
 
161
        $isfrontpage = ($this->course->id == SITEID);
162
        $accesssince = 0;
163
        // Whether to match on users who HAVE accessed since the given time (ie false is 'inactive for more than x').
164
        $matchaccesssince = false;
165
 
166
        // The alias for the subquery that fetches all distinct course users.
167
        $usersubqueryalias = 'targetusers';
168
        // The alias for {user} within the distinct user subquery.
169
        $inneruseralias = 'udistinct';
170
        // Inner query that selects distinct users in a course who are not deleted.
171
        // Note: This ensures the outer (filtering) query joins on distinct users, avoiding the need for GROUP BY.
172
        $innerselect = "SELECT DISTINCT {$inneruseralias}.id";
173
        $innerjoins = ["{user} {$inneruseralias}"];
174
        $innerwhere = "WHERE {$inneruseralias}.deleted = 0 AND {$inneruseralias}.id <> :siteguest";
175
        $params = ['siteguest' => $CFG->siteguest];
176
 
177
        $outerjoins = ["JOIN {user} u ON u.id = {$usersubqueryalias}.id"];
178
        $wheres = [];
179
 
180
        if ($this->filterset->has_filter('accesssince')) {
181
            $accesssince = $this->filterset->get_filter('accesssince')->current();
182
 
183
            // Last access filtering only supports matching or not matching, not any/all/none.
184
            $jointypenone = $this->filterset->get_filter('accesssince')::JOINTYPE_NONE;
185
            if ($this->filterset->get_filter('accesssince')->get_join_type() === $jointypenone) {
186
                $matchaccesssince = true;
187
            }
188
        }
189
 
190
        [
191
            // SQL that forms part of the filter.
192
            'sql' => $esql,
193
            // SQL for enrolment filtering that must always be applied (eg due to capability restrictions).
194
            'forcedsql' => $esqlforced,
195
            'params' => $eparams,
196
        ] = $this->get_enrolled_sql();
197
        $params = array_merge($params, $eparams);
198
 
199
        // Get the fields for all contexts because there is a special case later where it allows
200
        // matches of fields you can't access if they are on your own account.
201
        $userfields = fields::for_identity(null)->with_userpic();
202
        ['selects' => $userfieldssql, 'joins' => $userfieldsjoin, 'params' => $userfieldsparams, 'mappings' => $mappings] =
203
                (array)$userfields->get_sql('u', true);
204
        if ($userfieldsjoin) {
205
            $outerjoins[] = $userfieldsjoin;
206
            $params = array_merge($params, $userfieldsparams);
207
        }
208
 
209
        // Include any compulsory enrolment SQL (eg capability related filtering that must be applied).
210
        if (!empty($esqlforced)) {
211
            $outerjoins[] = "JOIN ({$esqlforced}) fef ON fef.id = u.id";
212
        }
213
 
214
        // Include any enrolment related filtering.
215
        if (!empty($esql)) {
216
            $outerjoins[] = "LEFT JOIN ({$esql}) ef ON ef.id = u.id";
217
            $wheres[] = 'ef.id IS NOT NULL';
218
        }
219
 
220
        if ($isfrontpage) {
221
            $outerselect = "SELECT u.lastaccess $userfieldssql";
222
            if ($accesssince) {
223
                $wheres[] = user_get_user_lastaccess_sql($accesssince, 'u', $matchaccesssince);
224
            }
225
        } else {
226
            $outerselect = "SELECT COALESCE(ul.timeaccess, 0) AS lastaccess $userfieldssql";
227
            // Not everybody has accessed the course yet.
228
            $outerjoins[] = 'LEFT JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = :courseid2)';
229
            $params['courseid2'] = $this->course->id;
230
            if ($accesssince) {
231
                $wheres[] = user_get_course_lastaccess_sql($accesssince, 'ul', $matchaccesssince);
232
            }
233
 
234
            // Make sure we only ever fetch users in the course (regardless of enrolment filters).
235
            $innerjoins[] = "JOIN {user_enrolments} ue ON ue.userid = {$inneruseralias}.id";
236
            $innerjoins[] = 'JOIN {enrol} e ON e.id = ue.enrolid
237
                                      AND e.courseid = :courseid1';
238
            $params['courseid1'] = $this->course->id;
239
        }
240
 
241
        // Performance hacks - we preload user contexts together with accounts.
242
        $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
243
        $ccjoin = 'LEFT JOIN {context} ctx ON (ctx.instanceid = u.id AND ctx.contextlevel = :contextlevel)';
244
        $params['contextlevel'] = CONTEXT_USER;
245
        $outerselect .= $ccselect;
246
        $outerjoins[] = $ccjoin;
247
 
248
        // Apply any role filtering.
249
        if ($this->filterset->has_filter('roles')) {
250
            [
251
                'where' => $roleswhere,
252
                'params' => $rolesparams,
253
            ] = $this->get_roles_sql();
254
 
255
            if (!empty($roleswhere)) {
256
                $wheres[] = "({$roleswhere})";
257
            }
258
 
259
            if (!empty($rolesparams)) {
260
                $params = array_merge($params, $rolesparams);
261
            }
262
        }
263
 
264
        // Apply any country filtering.
265
        if ($this->filterset->has_filter('country')) {
266
            [
267
                'where' => $countrywhere,
268
                'params' => $countryparams,
269
            ] = $this->get_country_sql();
270
 
271
            if (!empty($countrywhere)) {
272
                $wheres[] = "($countrywhere)";
273
            }
274
 
275
            if (!empty($countryparams)) {
276
                $params = array_merge($params, $countryparams);
277
            }
278
        }
279
 
280
        // Apply any keyword text searches.
281
        if ($this->filterset->has_filter('keywords')) {
282
            [
283
                'where' => $keywordswhere,
284
                'params' => $keywordsparams,
285
            ] = $this->get_keywords_search_sql($mappings);
286
 
287
            if (!empty($keywordswhere)) {
288
                $wheres[] = "({$keywordswhere})";
289
            }
290
 
291
            if (!empty($keywordsparams)) {
292
                $params = array_merge($params, $keywordsparams);
293
            }
294
        }
295
 
296
        // Add any supplied additional forced WHERE clauses.
297
        if (!empty($additionalwhere)) {
298
            $innerwhere .= " AND ({$additionalwhere})";
299
            $params = array_merge($params, $additionalparams);
300
        }
301
 
302
        // Prepare final values.
303
        $outerjoinsstring = implode("\n", $outerjoins);
304
        $innerjoinsstring = implode("\n", $innerjoins);
305
        if ($wheres) {
306
            switch ($this->filterset->get_join_type()) {
307
                case $this->filterset::JOINTYPE_ALL:
308
                    $wherenot = '';
309
                    $wheresjoin = ' AND ';
310
                    break;
311
                case $this->filterset::JOINTYPE_NONE:
312
                    $wherenot = ' NOT ';
313
                    $wheresjoin = ' AND NOT ';
314
 
315
                    // Some of the $where conditions may begin with `NOT` which results in `AND NOT NOT ...`.
316
                    // To prevent this from breaking on Oracle the inner WHERE clause is wrapped in brackets, making it
317
                    // `AND NOT (NOT ...)` which is valid in all DBs.
318
                    $wheres = array_map(function($where) {
319
                        return "({$where})";
320
                    }, $wheres);
321
 
322
                    break;
323
                default:
324
                    // Default to 'Any' jointype.
325
                    $wherenot = '';
326
                    $wheresjoin = ' OR ';
327
                    break;
328
            }
329
 
330
            $outerwhere = 'WHERE ' . $wherenot . implode($wheresjoin, $wheres);
331
        } else {
332
            $outerwhere = '';
333
        }
334
 
335
        return [
336
            'subqueryalias' => $usersubqueryalias,
337
            'outerselect' => $outerselect,
338
            'innerselect' => $innerselect,
339
            'outerjoins' => $outerjoinsstring,
340
            'innerjoins' => $innerjoinsstring,
341
            'outerwhere' => $outerwhere,
342
            'innerwhere' => $innerwhere,
343
            'params' => $params,
344
        ];
345
    }
346
 
347
    /**
348
     * Prepare SQL and associated parameters for users enrolled in the course.
349
     *
350
     * @return array SQL query data in the format ['sql' => '', 'forcedsql' => '', 'params' => []].
351
     */
352
    protected function get_enrolled_sql(): array {
353
        global $USER;
354
 
355
        $isfrontpage = ($this->context->instanceid == SITEID);
356
        $prefix = 'eu_';
357
        $filteruid = "{$prefix}u.id";
358
        $sql = '';
359
        $joins = [];
360
        $wheres = [];
361
        $params = [];
362
        // It is possible some statements must always be included (in addition to any filtering).
363
        $forcedprefix = "f{$prefix}";
364
        $forceduid = "{$forcedprefix}u.id";
365
        $forcedsql = '';
366
        $forcedjoins = [];
367
        $forcedwhere = "{$forcedprefix}u.deleted = 0";
368
 
369
        if (!$isfrontpage) {
370
            // Prepare any enrolment method filtering.
371
            [
372
                'joins' => $methodjoins,
373
                'where' => $wheres[],
374
                'params' => $methodparams,
375
            ] = $this->get_enrol_method_sql($filteruid);
376
 
377
            // Prepare any status filtering.
378
            [
379
                'joins' => $statusjoins,
380
                'where' => $statuswhere,
381
                'params' => $statusparams,
382
                'forcestatus' => $forcestatus,
383
            ] = $this->get_status_sql($filteruid, $forceduid, $forcedprefix);
384
 
385
            if ($forcestatus) {
386
                // Force filtering by active participants if user does not have capability to view suspended.
387
                $forcedjoins = array_merge($forcedjoins, $statusjoins);
388
                $statusjoins = [];
389
                $forcedwhere .= " AND ({$statuswhere})";
390
            } else {
391
                $wheres[] = $statuswhere;
392
            }
393
 
394
            $joins = array_merge($joins, $methodjoins, $statusjoins);
395
            $params = array_merge($params, $methodparams, $statusparams);
396
        }
397
 
398
        $groupids = [];
399
 
400
        if ($this->filterset->has_filter('groups')) {
401
            $groupids = $this->filterset->get_filter('groups')->get_filter_values();
402
        }
403
 
404
        // Force additional groups filtering if required due to lack of capabilities.
405
        // Note: This means results will always be limited to allowed groups, even if the user applies their own groups filtering.
406
        $canaccessallgroups = has_capability('moodle/site:accessallgroups', $this->context);
407
        $forcegroups = ($this->course->groupmode == SEPARATEGROUPS && !$canaccessallgroups);
408
 
409
        if ($forcegroups) {
410
            $allowedgroupids = array_keys(groups_get_all_groups($this->course->id, $USER->id));
411
 
412
            // Users not in any group in a course with separate groups mode should not be able to access the participants filter.
413
            if (empty($allowedgroupids)) {
414
                // The UI does not support this, so it should not be reachable unless someone is trying to bypass the restriction.
415
                throw new \coding_exception('User must be part of a group to filter by participants.');
416
            }
417
 
418
            $forceduid = "{$forcedprefix}u.id";
419
            $forcedjointype = $this->get_groups_jointype(\core_table\local\filter\filter::JOINTYPE_ANY);
420
            $forcedgroupjoin = groups_get_members_join($allowedgroupids, $forceduid, $this->context, $forcedjointype);
421
 
422
            $forcedjoins[] = $forcedgroupjoin->joins;
423
            $forcedwhere .= " AND ({$forcedgroupjoin->wheres})";
424
 
425
            $params = array_merge($params, $forcedgroupjoin->params);
426
 
427
            // Remove any filtered groups the user does not have access to.
428
            $groupids = array_intersect($allowedgroupids, $groupids);
429
        }
430
 
431
        // Prepare any user defined groups filtering.
432
        if ($groupids) {
433
            $groupjoin = groups_get_members_join($groupids, $filteruid, $this->context, $this->get_groups_jointype());
434
 
435
            $joins[] = $groupjoin->joins;
436
            $params = array_merge($params, $groupjoin->params);
437
            if (!empty($groupjoin->wheres)) {
438
                $wheres[] = $groupjoin->wheres;
439
            }
440
        }
441
 
442
        // Combine the relevant filters and prepare the query.
443
        $joins = array_filter($joins);
444
        if (!empty($joins)) {
445
            $joinsql = implode("\n", $joins);
446
 
447
            $sql = "SELECT DISTINCT {$prefix}u.id
448
                               FROM {user} {$prefix}u
449
                                    {$joinsql}
450
                              WHERE {$prefix}u.deleted = 0";
451
        }
452
 
453
        $wheres = array_filter($wheres);
454
        if (!empty($wheres)) {
455
            if ($this->filterset->get_join_type() === $this->filterset::JOINTYPE_ALL) {
456
                $wheresql = '(' . implode(') AND (', $wheres) . ')';
457
            } else {
458
                $wheresql = '(' . implode(') OR (', $wheres) . ')';
459
            }
460
 
461
            $sql .= " AND ({$wheresql})";
462
        }
463
 
464
        // Prepare any SQL that must be applied.
465
        if (!empty($forcedjoins)) {
466
            $forcedjoinsql = implode("\n", $forcedjoins);
467
            $forcedsql = "SELECT DISTINCT {$forcedprefix}u.id
468
                                     FROM {user} {$forcedprefix}u
469
                                          {$forcedjoinsql}
470
                                    WHERE {$forcedwhere}";
471
        }
472
 
473
        return [
474
            'sql' => $sql,
475
            'forcedsql' => $forcedsql,
476
            'params' => $params,
477
        ];
478
    }
479
 
480
    /**
481
     * Prepare the enrolment methods filter SQL content.
482
     *
483
     * @param string $useridcolumn User ID column used in the calling query, e.g. u.id
484
     * @return array SQL query data in the format ['joins' => [], 'where' => '', 'params' => []].
485
     */
486
    protected function get_enrol_method_sql($useridcolumn): array {
487
        global $DB;
488
 
489
        $prefix = 'ejm_';
490
        $joins  = [];
491
        $where = '';
492
        $params = [];
493
        $enrolids = [];
494
 
495
        if ($this->filterset->has_filter('enrolments')) {
496
            $enrolids = $this->filterset->get_filter('enrolments')->get_filter_values();
497
        }
498
 
499
        if (!empty($enrolids)) {
500
            $jointype = $this->filterset->get_filter('enrolments')->get_join_type();
501
 
502
            // Handle 'All' join type.
503
            if ($jointype === $this->filterset->get_filter('enrolments')::JOINTYPE_ALL ||
504
                    $jointype === $this->filterset->get_filter('enrolments')::JOINTYPE_NONE) {
505
                $allwheres = [];
506
 
507
                foreach ($enrolids as $i => $enrolid) {
508
                    $thisprefix = "{$prefix}{$i}";
509
                    list($enrolidsql, $enrolidparam) = $DB->get_in_or_equal($enrolid, SQL_PARAMS_NAMED, $thisprefix);
510
 
511
                    $joins[] = "LEFT JOIN {enrol} {$thisprefix}e
512
                                       ON ({$thisprefix}e.id {$enrolidsql}
513
                                      AND {$thisprefix}e.courseid = :{$thisprefix}courseid)";
514
                    $joins[] = "LEFT JOIN {user_enrolments} {$thisprefix}ue
515
                                       ON {$thisprefix}ue.userid = {$useridcolumn}
516
                                      AND {$thisprefix}ue.enrolid = {$thisprefix}e.id";
517
 
518
                    if ($jointype === $this->filterset->get_filter('enrolments')::JOINTYPE_ALL) {
519
                        $allwheres[] = "{$thisprefix}ue.id IS NOT NULL";
520
                    } else {
521
                        // Ensure participants do not match any of the filtered methods when joining by 'None'.
522
                        $allwheres[] = "{$thisprefix}ue.id IS NULL";
523
                    }
524
 
525
                    $params["{$thisprefix}courseid"] = $this->course->id;
526
                    $params = array_merge($params, $enrolidparam);
527
                }
528
 
529
                if (!empty($allwheres)) {
530
                    $where = implode(' AND ', $allwheres);
531
                }
532
            } else {
533
                // Handle the 'Any'join type.
534
 
535
                list($enrolidssql, $enrolidsparams) = $DB->get_in_or_equal($enrolids, SQL_PARAMS_NAMED, $prefix);
536
 
537
                $joins[] = "LEFT JOIN {enrol} {$prefix}e
538
                                   ON ({$prefix}e.id {$enrolidssql}
539
                                  AND {$prefix}e.courseid = :{$prefix}courseid)";
540
                $joins[] = "LEFT JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$useridcolumn}
541
                                                              AND {$prefix}ue.enrolid = {$prefix}e.id";
542
                $where = "{$prefix}ue.id IS NOT NULL";
543
 
544
                $params["{$prefix}courseid"] = $this->course->id;
545
                $params = array_merge($params, $enrolidsparams);
546
            }
547
        }
548
 
549
        return [
550
            'joins' => $joins,
551
            'where' => $where,
552
            'params' => $params,
553
        ];
554
    }
555
 
556
    /**
557
     * Prepare the status filter SQL content.
558
     * Note: Users who cannot view suspended users will always have their results filtered to only show active participants.
559
     *
560
     * @param string $filteruidcolumn User ID column used in the calling query, e.g. eu_u.id
561
     * @param string $forceduidcolumn User ID column used in any forced query, e.g. feu_u.id
562
     * @param string $forcedprefix The prefix to use if forced filtering is required
563
     * @return array SQL query data in the format ['joins' => [], 'where' => '', 'params' => [], 'forcestatus' => true]
564
     */
565
    protected function get_status_sql($filteruidcolumn, $forceduidcolumn, $forcedprefix): array {
566
        $prefix = $forcedprefix;
567
        $useridcolumn = $forceduidcolumn;
568
        $joins  = [];
569
        $where = '';
570
        $params = [];
571
        $forcestatus = true;
572
 
573
        // By default we filter to show users with active status only.
574
        $statusids = [ENROL_USER_ACTIVE];
575
        $statusjointype = $this->filterset::JOINTYPE_DEFAULT;
576
 
577
        // Allow optional status filtering if the user has relevant capabilities.
578
        if (has_capability('moodle/course:enrolreview', $this->context) &&
579
                (has_capability('moodle/course:viewsuspendedusers', $this->context))) {
580
            $forcestatus = false;
581
            $prefix = 'ejs_';
582
            $useridcolumn = $filteruidcolumn;
583
 
584
            // Default to no filtering if capabilities allow for it.
585
            $statusids = [];
586
 
587
            if ($this->filterset->has_filter('status')) {
588
                $statusjointype = $this->filterset->get_filter('status')->get_join_type();
589
                $statusfiltervalues = $this->filterset->get_filter('status')->get_filter_values();
590
 
591
                // If values are set for the status filter, use them.
592
                if (!empty($statusfiltervalues)) {
593
                    $statusids = $statusfiltervalues;
594
                }
595
            }
596
        }
597
 
598
        if (!empty($statusids)) {
599
            $enroljoin = 'JOIN {enrol} %1$se ON %1$se.id = %1$sue.enrolid
600
                                                  AND %1$se.courseid = :%1$scourseid';
601
 
602
            $whereactive = '(%1$sue.status = :%2$sactive
603
                          AND %1$se.status = :%2$senabled
604
                      AND %1$sue.timestart < :%2$snow1
605
                       AND (%1$sue.timeend = 0
606
                         OR %1$sue.timeend > :%2$snow2))';
607
 
608
            $wheresuspended = '(%1$sue.status = :%2$ssuspended
609
                             OR %1$se.status != :%2$senabled
610
                         OR %1$sue.timestart >= :%2$snow1
611
                           OR (%1$sue.timeend > 0
612
                          AND %1$sue.timeend <= :%2$snow2))';
613
 
614
            // Round 'now' time to help DB caching.
615
            $now = round(time(), -2);
616
 
617
            switch ($statusjointype) {
618
                case $this->filterset::JOINTYPE_ALL:
619
                    $joinwheres = [];
620
 
621
                    foreach ($statusids as $i => $statusid) {
622
                        $joinprefix = "{$prefix}{$i}";
623
                        $joins[] = "JOIN {user_enrolments} {$joinprefix}ue ON {$joinprefix}ue.userid = {$useridcolumn}";
624
 
625
                        if ($statusid === ENROL_USER_ACTIVE) {
626
                            // Conditions to be met if user filtering by active.
627
                            $joinwheres[] = sprintf($whereactive, $joinprefix, $joinprefix);
628
 
629
                            $activeparams = [
630
                                "{$joinprefix}active" => ENROL_USER_ACTIVE,
631
                                "{$joinprefix}enabled" => ENROL_INSTANCE_ENABLED,
632
                                "{$joinprefix}now1"   => $now,
633
                                "{$joinprefix}now2"   => $now,
634
                                "{$joinprefix}courseid"   => $this->course->id,
635
                            ];
636
 
637
                            $params = array_merge($params, $activeparams);
638
                        } else {
639
                            // Conditions to be met if filtering by suspended (currently the only other status).
640
                            $joinwheres[] = sprintf($wheresuspended, $joinprefix, $joinprefix);
641
 
642
                            $suspendedparams = [
643
                                "{$joinprefix}suspended" => ENROL_USER_SUSPENDED,
644
                                "{$joinprefix}enabled" => ENROL_INSTANCE_ENABLED,
645
                                "{$joinprefix}now1"   => $now,
646
                                "{$joinprefix}now2"   => $now,
647
                                "{$joinprefix}courseid"   => $this->course->id,
648
                            ];
649
 
650
                            $params = array_merge($params, $suspendedparams);
651
                        }
652
 
653
                        $joins[] = sprintf($enroljoin, $joinprefix);
654
                    }
655
 
656
                    $where = implode(' AND ', $joinwheres);
657
                    break;
658
 
659
                case $this->filterset::JOINTYPE_NONE:
660
                    // Should always be enrolled, just not in any of the filtered statuses.
661
                    $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$useridcolumn}";
662
                    $joins[] = sprintf($enroljoin, $prefix);
663
                    $joinwheres = [];
664
                    $params["{$prefix}courseid"] = $this->course->id;
665
 
666
                    foreach ($statusids as $i => $statusid) {
667
                        $paramprefix = "{$prefix}{$i}";
668
 
669
                        if ($statusid === ENROL_USER_ACTIVE) {
670
                            // Conditions to be met if user filtering by active.
671
                            $joinwheres[] = sprintf("NOT {$whereactive}", $prefix, $paramprefix);
672
 
673
                            $activeparams = [
674
                                "{$paramprefix}active" => ENROL_USER_ACTIVE,
675
                                "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED,
676
                                "{$paramprefix}now1"   => $now,
677
                                "{$paramprefix}now2"   => $now,
678
                            ];
679
 
680
                            $params = array_merge($params, $activeparams);
681
                        } else {
682
                            // Conditions to be met if filtering by suspended (currently the only other status).
683
                            $joinwheres[] = sprintf("NOT {$wheresuspended}", $prefix, $paramprefix);
684
 
685
                            $suspendedparams = [
686
                                "{$paramprefix}suspended" => ENROL_USER_SUSPENDED,
687
                                "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED,
688
                                "{$paramprefix}now1"   => $now,
689
                                "{$paramprefix}now2"   => $now,
690
                            ];
691
 
692
                            $params = array_merge($params, $suspendedparams);
693
                        }
694
                    }
695
 
696
                    $where = '(' . implode(' AND ', $joinwheres) . ')';
697
                    break;
698
 
699
                default:
700
                    // Handle the 'Any' join type.
701
 
702
                    $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$useridcolumn}";
703
                    $joins[] = sprintf($enroljoin, $prefix);
704
                    $joinwheres = [];
705
                    $params["{$prefix}courseid"] = $this->course->id;
706
 
707
                    foreach ($statusids as $i => $statusid) {
708
                        $paramprefix = "{$prefix}{$i}";
709
 
710
                        if ($statusid === ENROL_USER_ACTIVE) {
711
                            // Conditions to be met if user filtering by active.
712
                            $joinwheres[] = sprintf($whereactive, $prefix, $paramprefix);
713
 
714
                            $activeparams = [
715
                                "{$paramprefix}active" => ENROL_USER_ACTIVE,
716
                                "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED,
717
                                "{$paramprefix}now1"   => $now,
718
                                "{$paramprefix}now2"   => $now,
719
                            ];
720
 
721
                            $params = array_merge($params, $activeparams);
722
                        } else {
723
                            // Conditions to be met if filtering by suspended (currently the only other status).
724
                            $joinwheres[] = sprintf($wheresuspended, $prefix, $paramprefix);
725
 
726
                            $suspendedparams = [
727
                                "{$paramprefix}suspended" => ENROL_USER_SUSPENDED,
728
                                "{$paramprefix}enabled" => ENROL_INSTANCE_ENABLED,
729
                                "{$paramprefix}now1"   => $now,
730
                                "{$paramprefix}now2"   => $now,
731
                            ];
732
 
733
                            $params = array_merge($params, $suspendedparams);
734
                        }
735
                    }
736
 
737
                    $where = '(' . implode(' OR ', $joinwheres) . ')';
738
                    break;
739
            }
740
        }
741
 
742
        return [
743
            'joins' => $joins,
744
            'where' => $where,
745
            'params' => $params,
746
            'forcestatus' => $forcestatus,
747
        ];
748
    }
749
 
750
    /**
751
     * Fetch the groups filter's grouplib jointype, based on its filterset jointype.
752
     * This mapping is to ensure compatibility between the two, should their values ever differ.
753
     *
754
     * @param int|null $forcedjointype If set, specifies the join type to fetch mapping for (used when applying forced filtering).
755
     *                            If null, then user defined filter join type is used.
756
     * @return int
757
     */
758
    protected function get_groups_jointype(?int $forcedjointype = null): int {
759
 
760
        // If applying forced groups filter and no manual groups filtering is applied, add an empty filter so we can map the join.
761
        if (!is_null($forcedjointype) && !$this->filterset->has_filter('groups')) {
762
            $this->filterset->add_filter(new \core_table\local\filter\integer_filter('groups'));
763
        }
764
 
765
        $groupsfilter = $this->filterset->get_filter('groups');
766
 
767
        if (is_null($forcedjointype)) {
768
            // Fetch join type mapping for a user supplied groups filtering.
769
            $filterjointype = $groupsfilter->get_join_type();
770
        } else {
771
            // Fetch join type mapping for forced groups filtering.
772
            $filterjointype = $forcedjointype;
773
        }
774
 
775
        switch ($filterjointype) {
776
            case $groupsfilter::JOINTYPE_NONE:
777
                $groupsjoin = GROUPS_JOIN_NONE;
778
                break;
779
            case $groupsfilter::JOINTYPE_ALL:
780
                $groupsjoin = GROUPS_JOIN_ALL;
781
                break;
782
            default:
783
                // Default to ANY jointype.
784
                $groupsjoin = GROUPS_JOIN_ANY;
785
                break;
786
        }
787
 
788
        return $groupsjoin;
789
    }
790
 
791
    /**
792
     * Prepare SQL where clause and associated parameters for any roles filtering being performed.
793
     *
794
     * @return array SQL query data in the format ['where' => '', 'params' => []].
795
     */
796
    protected function get_roles_sql(): array {
797
        global $DB;
798
 
799
        $where = '';
800
        $params = [];
801
 
802
        // Limit list to users with some role only.
803
        if ($this->filterset->has_filter('roles')) {
804
            $rolesfilter = $this->filterset->get_filter('roles');
805
 
806
            $roleids = $rolesfilter->get_filter_values();
807
            $jointype = $rolesfilter->get_join_type();
808
 
809
            // Determine how to match values in the query.
810
            $matchinsql = 'IN';
811
            switch ($jointype) {
812
                case $rolesfilter::JOINTYPE_ALL:
813
                    $wherejoin = ' AND ';
814
                    break;
815
                case $rolesfilter::JOINTYPE_NONE:
816
                    $wherejoin = ' AND NOT ';
817
                    $matchinsql = 'NOT IN';
818
                    break;
819
                default:
820
                    // Default to 'Any' jointype.
821
                    $wherejoin = ' OR ';
822
                    break;
823
            }
824
 
825
            // We want to query both the current context and parent contexts.
826
            $rolecontextids = $this->context->get_parent_context_ids(true);
827
 
828
            // Get users without any role, if needed.
829
            if (($withoutkey = array_search(-1, $roleids)) !== false) {
830
                list($relatedctxsql1, $norolectxparams) = $DB->get_in_or_equal($rolecontextids, SQL_PARAMS_NAMED, 'relatedctx');
831
 
832
                if ($jointype === $rolesfilter::JOINTYPE_NONE) {
833
                    $where .= "(u.id IN (SELECT userid FROM {role_assignments} WHERE contextid {$relatedctxsql1}))";
834
                } else {
835
                    $where .= "(u.id NOT IN (SELECT userid FROM {role_assignments} WHERE contextid {$relatedctxsql1}))";
836
                }
837
 
838
                $params = array_merge($params, $norolectxparams);
839
 
840
                if ($withoutkey !== false) {
841
                    unset($roleids[$withoutkey]);
842
                }
843
 
844
                // Join if any roles will be included.
845
                if (!empty($roleids)) {
846
                    // The NOT case is replaced with AND to prevent a double negative.
847
                    $where .= $jointype === $rolesfilter::JOINTYPE_NONE ? ' AND ' : $wherejoin;
848
                }
849
            }
850
 
851
            // Get users with specified roles, if needed.
852
            if (!empty($roleids)) {
853
                // All case - need one WHERE per filtered role.
854
                if ($rolesfilter::JOINTYPE_ALL === $jointype) {
855
                    $numroles = count($roleids);
856
                    $rolecount = 1;
857
 
858
                    foreach ($roleids as $roleid) {
859
                        list($relatedctxsql, $relctxparams) = $DB->get_in_or_equal($rolecontextids, SQL_PARAMS_NAMED, 'relatedctx');
860
                        list($roleidssql, $roleidparams) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'roleids');
861
 
862
                        $where .= "(u.id IN (
863
                                     SELECT userid
864
                                       FROM {role_assignments}
865
                                      WHERE roleid {$roleidssql}
866
                                        AND contextid {$relatedctxsql})
867
                                   )";
868
 
869
                        if ($rolecount < $numroles) {
870
                            $where .= $wherejoin;
871
                            $rolecount++;
872
                        }
873
 
874
                        $params = array_merge($params, $roleidparams, $relctxparams);
875
                    }
876
 
877
                } else {
878
                    // Any / None cases - need one WHERE to cover all filtered roles.
879
                    list($relatedctxsql, $relctxparams) = $DB->get_in_or_equal($rolecontextids, SQL_PARAMS_NAMED, 'relatedctx');
880
                    list($roleidssql, $roleidsparams) = $DB->get_in_or_equal($roleids, SQL_PARAMS_NAMED, 'roleids');
881
 
882
                    $where .= "(u.id {$matchinsql} (
883
                                 SELECT userid
884
                                   FROM {role_assignments}
885
                                  WHERE roleid {$roleidssql}
886
                                    AND contextid {$relatedctxsql})
887
                               )";
888
 
889
                    $params = array_merge($params, $roleidsparams, $relctxparams);
890
                }
891
            }
892
        }
893
 
894
        return [
895
            'where' => $where,
896
            'params' => $params,
897
        ];
898
    }
899
 
900
    /**
901
     * Prepare SQL where clause and associated parameters for country filtering
902
     *
903
     * @return array SQL query data in the format ['where' => '', 'params' => []].
904
     */
905
    protected function get_country_sql(): array {
906
        global $DB;
907
 
908
        $where = '';
909
        $params = [];
910
 
911
        $countryfilter = $this->filterset->get_filter('country');
912
        if ($countrycodes = $countryfilter->get_filter_values()) {
913
            // If filter type is "None", then we negate the comparison.
914
            [$countrywhere, $params] = $DB->get_in_or_equal($countrycodes, SQL_PARAMS_NAMED, 'country',
915
                $countryfilter->get_join_type() !== $countryfilter::JOINTYPE_NONE);
916
 
917
            $where = "(u.country {$countrywhere})";
918
        }
919
 
920
        return [
921
            'where' => $where,
922
            'params' => $params,
923
        ];
924
    }
925
 
926
    /**
927
     * Prepare SQL where clause and associated parameters for any keyword searches being performed.
928
     *
929
     * @param array $mappings Array of field mappings (fieldname => SQL code for the value)
930
     * @return array SQL query data in the format ['where' => '', 'params' => []].
931
     */
932
    protected function get_keywords_search_sql(array $mappings): array {
933
        global $CFG, $DB, $USER;
934
 
935
        $keywords = [];
936
        $where = '';
937
        $params = [];
938
        $keywordsfilter = $this->filterset->get_filter('keywords');
939
        $jointype = $keywordsfilter->get_join_type();
940
        // None join types in both filter row and filterset require additional 'not null' handling for accurate keywords matches.
941
        $notjoin = false;
942
 
943
        // Determine how to match values in the query.
944
        switch ($jointype) {
945
            case $keywordsfilter::JOINTYPE_ALL:
946
                $wherejoin = ' AND ';
947
                break;
948
            case $keywordsfilter::JOINTYPE_NONE:
949
                $wherejoin = ' AND NOT ';
950
                $notjoin = true;
951
                break;
952
            default:
953
                // Default to 'Any' jointype.
954
                $wherejoin = ' OR ';
955
                break;
956
        }
957
 
958
        // Handle filterset None join type.
959
        if ($this->filterset->get_join_type() === $this->filterset::JOINTYPE_NONE) {
960
            $notjoin = true;
961
        }
962
 
963
        if ($this->filterset->has_filter('keywords')) {
964
            $keywords = $keywordsfilter->get_filter_values();
965
        }
966
 
967
        $canviewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
968
 
969
        foreach ($keywords as $index => $keyword) {
970
            $searchkey1 = 'search' . $index . '1';
971
            $searchkey2 = 'search' . $index . '2';
972
            $searchkey3 = 'search' . $index . '3';
973
            $searchkey4 = 'search' . $index . '4';
974
            $searchkey5 = 'search' . $index . '5';
975
            $searchkey6 = 'search' . $index . '6';
976
            $searchkey7 = 'search' . $index . '7';
977
 
978
            $conditions = [];
979
 
980
            // Search by fullname.
981
            [$fullname, $fullnameparams] = fields::get_sql_fullname('u', $canviewfullnames);
982
            $conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
983
            $params = array_merge($params, $fullnameparams);
984
 
985
            // Search by email.
986
            $email = $DB->sql_like('email', ':' . $searchkey2, false, false);
987
 
988
            if ($notjoin) {
989
                $email = "(email IS NOT NULL AND {$email})";
990
            }
991
 
992
            if (!in_array('email', $this->userfields)) {
993
                $maildisplay = 'maildisplay' . $index;
994
                $userid1 = 'userid' . $index . '1';
995
                // Prevent users who hide their email address from being found by others
996
                // who aren't allowed to see hidden email addresses.
997
                $email = "(". $email ." AND (" .
998
                        "u.maildisplay <> :$maildisplay " .
999
                        "OR u.id = :$userid1". // Users can always find themselves.
1000
                        "))";
1001
                $params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
1002
                $params[$userid1] = $USER->id;
1003
            }
1004
 
1005
            $conditions[] = $email;
1006
 
1007
            // Search by idnumber.
1008
            $idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
1009
 
1010
            if ($notjoin) {
1011
                $idnumber = "(idnumber IS NOT NULL AND  {$idnumber})";
1012
            }
1013
 
1014
            if (!in_array('idnumber', $this->userfields)) {
1015
                $userid2 = 'userid' . $index . '2';
1016
                // Users who aren't allowed to see idnumbers should at most find themselves
1017
                // when searching for an idnumber.
1018
                $idnumber = "(". $idnumber . " AND u.id = :$userid2)";
1019
                $params[$userid2] = $USER->id;
1020
            }
1021
 
1022
            $conditions[] = $idnumber;
1023
 
1024
            // Search all user identify fields.
1025
            $extrasearchfields = fields::get_identity_fields(null);
1026
            foreach ($extrasearchfields as $fieldindex => $extrasearchfield) {
1027
                if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
1028
                    // Already covered above.
1029
                    continue;
1030
                }
1031
                // The param must be short (max 32 characters) so don't include field name.
1032
                $param = $searchkey3 . '_ident' . $fieldindex;
1033
                $fieldsql = $mappings[$extrasearchfield];
1034
                $condition = $DB->sql_like($fieldsql, ':' . $param, false, false);
1035
                $params[$param] = "%$keyword%";
1036
 
1037
                if ($notjoin) {
1038
                    $condition = "($fieldsql IS NOT NULL AND {$condition})";
1039
                }
1040
 
1041
                if (!in_array($extrasearchfield, $this->userfields)) {
1042
                    // User cannot see this field, but allow match if their own account.
1043
                    $userid3 = 'userid' . $index . '3_ident' . $fieldindex;
1044
                    $condition = "(". $condition . " AND u.id = :$userid3)";
1045
                    $params[$userid3] = $USER->id;
1046
                }
1047
                $conditions[] = $condition;
1048
            }
1049
 
1050
            // Search by middlename.
1051
            $middlename = $DB->sql_like('middlename', ':' . $searchkey4, false, false);
1052
 
1053
            if ($notjoin) {
1054
                $middlename = "(middlename IS NOT NULL AND {$middlename})";
1055
            }
1056
 
1057
            $conditions[] = $middlename;
1058
 
1059
            // Search by alternatename.
1060
            $alternatename = $DB->sql_like('alternatename', ':' . $searchkey5, false, false);
1061
 
1062
            if ($notjoin) {
1063
                $alternatename = "(alternatename IS NOT NULL AND {$alternatename})";
1064
            }
1065
 
1066
            $conditions[] = $alternatename;
1067
 
1068
            // Search by firstnamephonetic.
1069
            $firstnamephonetic = $DB->sql_like('firstnamephonetic', ':' . $searchkey6, false, false);
1070
 
1071
            if ($notjoin) {
1072
                $firstnamephonetic = "(firstnamephonetic IS NOT NULL AND {$firstnamephonetic})";
1073
            }
1074
 
1075
            $conditions[] = $firstnamephonetic;
1076
 
1077
            // Search by lastnamephonetic.
1078
            $lastnamephonetic = $DB->sql_like('lastnamephonetic', ':' . $searchkey7, false, false);
1079
 
1080
            if ($notjoin) {
1081
                $lastnamephonetic = "(lastnamephonetic IS NOT NULL AND {$lastnamephonetic})";
1082
            }
1083
 
1084
            $conditions[] = $lastnamephonetic;
1085
 
1086
            if (!empty($where)) {
1087
                $where .= $wherejoin;
1088
            } else if ($jointype === $keywordsfilter::JOINTYPE_NONE) {
1089
                // Join type 'None' requires the WHERE to begin with NOT.
1090
                $where .= ' NOT ';
1091
            }
1092
 
1093
            $where .= "(". implode(" OR ", $conditions) .") ";
1094
            $params[$searchkey1] = "%$keyword%";
1095
            $params[$searchkey2] = "%$keyword%";
1096
            $params[$searchkey3] = "%$keyword%";
1097
            $params[$searchkey4] = "%$keyword%";
1098
            $params[$searchkey5] = "%$keyword%";
1099
            $params[$searchkey6] = "%$keyword%";
1100
            $params[$searchkey7] = "%$keyword%";
1101
        }
1102
 
1103
        return [
1104
            'where' => $where,
1105
            'params' => $params,
1106
        ];
1107
    }
1108
}