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
 * Code for ajax user selectors.
19
 *
20
 * @package   core_user
21
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
/**
26
 * The default size of a user selector.
27
 */
28
define('USER_SELECTOR_DEFAULT_ROWS', 20);
29
 
30
/**
31
 * Base class for user selectors.
32
 *
33
 * In your theme, you must give each user-selector a defined width. If the
34
 * user selector has name="myid", then the div myid_wrapper must have a width
35
 * specified.
36
 *
37
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
38
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
abstract class user_selector_base {
41
    /** @var string The control name (and id) in the HTML. */
42
    protected $name;
43
    /** @var array Extra fields to search on and return in addition to firstname and lastname. */
44
    protected $extrafields;
45
    /** @var object Context used for capability checks regarding this selector (does
46
     * not necessarily restrict user list) */
47
    protected $accesscontext;
48
    /** @var boolean Whether the conrol should allow selection of many users, or just one. */
49
    protected $multiselect = true;
50
    /** @var int The height this control should have, in rows. */
51
    protected $rows = USER_SELECTOR_DEFAULT_ROWS;
52
    /** @var array A list of userids that should not be returned by this control. */
53
    protected $exclude = array();
54
    /** @var array|null A list of the users who are selected. */
55
    protected $selected = null;
56
    /** @var boolean When the search changes, do we keep previously selected options that do
57
     * not match the new search term? */
58
    protected $preserveselected = false;
59
    /** @var boolean If only one user matches the search, should we select them automatically. */
60
    protected $autoselectunique = false;
61
    /** @var int When searching, do we only match the starts of fields (better performance)
62
     * or do we match occurrences anywhere or do we match exact the fields. */
63
    protected $searchtype = USER_SEARCH_STARTS_WITH;
64
    /** @var mixed This is used by get selected users */
65
    protected $validatinguserids = null;
66
 
67
    /**  @var boolean Used to ensure we only output the search options for one user selector on
68
     * each page. */
69
    private static $searchoptionsoutput = false;
70
 
71
    /** @var array JavaScript YUI3 Module definition */
72
    protected static $jsmodule = array(
73
                'name' => 'user_selector',
74
                'fullpath' => '/user/selector/module.js',
75
                'requires'  => array('node', 'event-custom', 'datasource', 'json', 'moodle-core-notification'),
76
                'strings' => array(
77
                    array('previouslyselectedusers', 'moodle', '%%SEARCHTERM%%'),
78
                    array('nomatchingusers', 'moodle', '%%SEARCHTERM%%'),
79
                    array('none', 'moodle')
80
                ));
81
 
82
    /** @var int this is used to define maximum number of users visible in list */
83
    public $maxusersperpage = 100;
84
 
85
    /** @var boolean Whether to override fullname() */
86
    public $viewfullnames = false;
87
 
88
    /** @var boolean Whether to include custom user profile fields */
89
    protected $includecustomfields = false;
90
    /** @var string User fields selects for custom fields. */
91
    protected $userfieldsselects = '';
92
    /** @var string User fields join for custom fields. */
93
    protected $userfieldsjoin = '';
94
    /** @var array User fields params for custom fields. */
95
    protected $userfieldsparams = [];
96
    /** @var array User fields mappings for custom fields. */
97
    protected $userfieldsmappings = [];
98
 
99
    /**
100
     * Constructor. Each subclass must have a constructor with this signature.
101
     *
102
     * @param string $name the control name/id for use in the HTML.
103
     * @param array $options other options needed to construct this selector.
104
     * You must be able to clone a userselector by doing new get_class($us)($us->get_name(), $us->get_options());
105
     */
106
    public function __construct($name, $options = array()) {
107
        global $CFG, $PAGE;
108
 
109
        // Initialise member variables from constructor arguments.
110
        $this->name = $name;
111
 
112
        // Use specified context for permission checks, system context if not specified.
113
        if (isset($options['accesscontext'])) {
114
            $this->accesscontext = $options['accesscontext'];
115
        } else {
116
            $this->accesscontext = context_system::instance();
117
        }
118
 
119
        $this->viewfullnames = has_capability('moodle/site:viewfullnames', $this->accesscontext);
120
 
121
        // Check if some legacy code tries to override $CFG->showuseridentity.
122
        if (isset($options['extrafields'])) {
123
            debugging('The user_selector classes do not support custom list of extra identity fields any more. '.
124
                'Instead, the user identity fields defined by the site administrator will be used to respect '.
125
                'the configured privacy setting.', DEBUG_DEVELOPER);
126
            unset($options['extrafields']);
127
        }
128
 
129
        if (isset($options['includecustomfields'])) {
130
            $this->includecustomfields = $options['includecustomfields'];
131
        } else {
132
            $this->includecustomfields = false;
133
        }
134
 
135
        // Populate the list of additional user identifiers to display.
136
        if ($this->includecustomfields) {
137
            $userfieldsapi = \core_user\fields::for_identity($this->accesscontext)->with_name();
138
            $this->extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
139
            [
140
                'selects' => $this->userfieldsselects,
141
                'joins' => $this->userfieldsjoin,
142
                'params' => $this->userfieldsparams,
143
                'mappings' => $this->userfieldsmappings
144
            ] = (array) $userfieldsapi->get_sql('u', true, '', '', false);
145
        } else {
146
            $this->extrafields = \core_user\fields::get_identity_fields($this->accesscontext, false);
147
        }
148
 
149
        if (isset($options['exclude']) && is_array($options['exclude'])) {
150
            $this->exclude = $options['exclude'];
151
        }
152
        if (isset($options['multiselect'])) {
153
            $this->multiselect = $options['multiselect'];
154
        }
155
 
156
        // Read the user prefs / optional_params that we use.
157
        $this->preserveselected = $this->initialise_option('userselector_preserveselected', $this->preserveselected);
158
        $this->autoselectunique = $this->initialise_option('userselector_autoselectunique', $this->autoselectunique);
159
        $this->searchtype = (int) $this->initialise_option('userselector_searchtype', $this->searchtype, PARAM_INT);
160
        if (!empty($CFG->maxusersperpage)) {
161
            $this->maxusersperpage = $CFG->maxusersperpage;
162
        }
163
    }
164
 
165
    /**
166
     * All to the list of user ids that this control will not select.
167
     *
168
     * For example, on the role assign page, we do not list the users who already have the role in question.
169
     *
170
     * @param array $arrayofuserids the user ids to exclude.
171
     */
172
    public function exclude($arrayofuserids) {
173
        $this->exclude = array_unique(array_merge($this->exclude, $arrayofuserids));
174
    }
175
 
176
    /**
177
     * Clear the list of excluded user ids.
178
     */
179
    public function clear_exclusions() {
180
        $this->exclude = array();
181
    }
182
 
183
    /**
184
     * Returns the list of user ids that this control will not select.
185
     *
186
     * @return array the list of user ids that this control will not select.
187
     */
188
    public function get_exclusions() {
189
        return clone($this->exclude);
190
    }
191
 
192
    /**
193
     * The users that were selected.
194
     *
195
     * This is a more sophisticated version of optional_param($this->name, array(), PARAM_INT) that validates the
196
     * returned list of ids against the rules for this user selector.
197
     *
198
     * @return array of user objects.
199
     */
200
    public function get_selected_users() {
201
        // Do a lazy load.
202
        if (is_null($this->selected)) {
203
            $this->selected = $this->load_selected_users();
204
        }
205
        return $this->selected;
206
    }
207
 
208
    /**
209
     * Convenience method for when multiselect is false (throws an exception if not).
210
     *
211
     * @throws moodle_exception
212
     * @return object the selected user object, or null if none.
213
     */
214
    public function get_selected_user() {
215
        if ($this->multiselect) {
216
            throw new moodle_exception('cannotcallusgetselecteduser');
217
        }
218
        $users = $this->get_selected_users();
219
        if (count($users) == 1) {
220
            return reset($users);
221
        } else if (count($users) == 0) {
222
            return null;
223
        } else {
224
            throw new moodle_exception('userselectortoomany');
225
        }
226
    }
227
 
228
    /**
229
     * Invalidates the list of selected users.
230
     *
231
     * If you update the database in such a way that it is likely to change the
232
     * list of users that this component is allowed to select from, then you
233
     * must call this method. For example, on the role assign page, after you have
234
     * assigned some roles to some users, you should call this.
235
     */
236
    public function invalidate_selected_users() {
237
        $this->selected = null;
238
    }
239
 
240
    /**
241
     * Output this user_selector as HTML.
242
     *
243
     * @param boolean $return if true, return the HTML as a string instead of outputting it.
244
     * @return mixed if $return is true, returns the HTML as a string, otherwise returns nothing.
245
     */
246
    public function display($return = false) {
247
        global $PAGE;
248
 
249
        // Get the list of requested users.
250
        $search = optional_param($this->name . '_searchtext', '', PARAM_RAW);
251
        if (optional_param($this->name . '_clearbutton', false, PARAM_BOOL)) {
252
            $search = '';
253
        }
254
        $groupedusers = $this->find_users($search);
255
 
256
        // Output the select.
257
        $name = $this->name;
258
        $multiselect = '';
259
        if ($this->multiselect) {
260
            $name .= '[]';
261
            $multiselect = 'multiple="multiple" ';
262
        }
263
        $output = '<div class="userselector" id="' . $this->name . '_wrapper">' . "\n" .
264
                '<select name="' . $name . '" id="' . $this->name . '" aria-live="polite" ' .
265
                $multiselect . 'size="' . $this->rows . '" class="form-control no-overflow">' . "\n";
266
 
267
        // Populate the select.
268
        $output .= $this->output_options($groupedusers, $search);
269
 
270
        // Output the search controls.
271
        $output .= "</select>\n<div class=\"d-flex flex-wrap align-items-center\">\n";
272
        $output .= '<input type="text" name="' . $this->name . '_searchtext" id="' .
273
                $this->name . '_searchtext" size="15" value="' . s($search) . '" class="form-control"/>';
274
        $output .= '<input type="submit" name="' . $this->name . '_searchbutton" id="' .
275
                $this->name . '_searchbutton" value="' . $this->search_button_caption() . '" class="btn btn-secondary"/>';
276
        $output .= '<input type="submit" name="' . $this->name . '_clearbutton" id="' .
277
                $this->name . '_clearbutton" value="' . get_string('clear') . '" class="btn btn-secondary"/>';
278
 
279
        // And the search options.
280
        if (!user_selector_base::$searchoptionsoutput) {
281
            $output .= print_collapsible_region_start('', 'userselector_options',
282
                get_string('searchoptions'), 'userselector_optionscollapsed', true, true);
283
            $output .= $this->option_checkbox('preserveselected', $this->preserveselected,
284
                get_string('userselectorpreserveselected'));
285
            $output .= $this->option_checkbox('autoselectunique', $this->autoselectunique,
286
                get_string('userselectorautoselectunique'));
287
            $output .= $this->output_searchtype_radios();
288
            $output .= print_collapsible_region_end(true);
289
 
290
            $PAGE->requires->js_init_call('M.core_user.init_user_selector_options_tracker', array(), false, self::$jsmodule);
291
            user_selector_base::$searchoptionsoutput = true;
292
        }
293
        $output .= "</div>\n</div>\n\n";
294
 
295
        // Initialise the ajax functionality.
296
        $output .= $this->initialise_javascript($search);
297
 
298
        // Return or output it.
299
        if ($return) {
300
            return $output;
301
        } else {
302
            echo $output;
303
        }
304
    }
305
 
306
    /**
307
     * The height this control will be displayed, in rows.
308
     *
309
     * @param integer $numrows the desired height.
310
     */
311
    public function set_rows($numrows) {
312
        $this->rows = $numrows;
313
    }
314
 
315
    /**
316
     * Returns the number of rows to display in this control.
317
     *
318
     * @return integer the height this control will be displayed, in rows.
319
     */
320
    public function get_rows() {
321
        return $this->rows;
322
    }
323
 
324
    /**
325
     * Whether this control will allow selection of many, or just one user.
326
     *
327
     * @param boolean $multiselect true = allow multiple selection.
328
     */
329
    public function set_multiselect($multiselect) {
330
        $this->multiselect = $multiselect;
331
    }
332
 
333
    /**
334
     * Returns true is multiselect should be allowed.
335
     *
336
     * @return boolean whether this control will allow selection of more than one user.
337
     */
338
    public function is_multiselect() {
339
        return $this->multiselect;
340
    }
341
 
342
    /**
343
     * Returns the id/name of this control.
344
     *
345
     * @return string the id/name that this control will have in the HTML.
346
     */
347
    public function get_name() {
348
        return $this->name;
349
    }
350
 
351
    /**
352
     * Set the user fields that are displayed in the selector in addition to the user's name.
353
     *
354
     * @param array $fields a list of field names that exist in the user table.
355
     */
356
    public function set_extra_fields($fields) {
357
        debugging('The user_selector classes do not support custom list of extra identity fields any more. '.
358
            'Instead, the user identity fields defined by the site administrator will be used to respect '.
359
            'the configured privacy setting.', DEBUG_DEVELOPER);
360
    }
361
 
362
    /**
363
     * Search the database for users matching the $search string, and any other
364
     * conditions that apply. The SQL for testing whether a user matches the
365
     * search string should be obtained by calling the search_sql method.
366
     *
367
     * This method is used both when getting the list of choices to display to
368
     * the user, and also when validating a list of users that was selected.
369
     *
370
     * When preparing a list of users to choose from ($this->is_validating()
371
     * return false) you should probably have an maximum number of users you will
372
     * return, and if more users than this match your search, you should instead
373
     * return a message generated by the too_many_results() method. However, you
374
     * should not do this when validating.
375
     *
376
     * If you are writing a new user_selector subclass, I strongly recommend you
377
     * look at some of the subclasses later in this file and in admin/roles/lib.php.
378
     * They should help you see exactly what you have to do.
379
     *
380
     * @param string $search the search string.
381
     * @return array An array of arrays of users. The array keys of the outer
382
     *      array should be the string names of optgroups. The keys of the inner
383
     *      arrays should be userids, and the values should be user objects
384
     *      containing at least the list of fields returned by the method
385
     *      required_fields_sql(). If a user object has a ->disabled property
386
     *      that is true, then that option will be displayed greyed out, and
387
     *      will not be returned by get_selected_users.
388
     */
389
    abstract public function find_users($search);
390
 
391
    /**
392
     *
393
     * Note: this function must be implemented if you use the search ajax field
394
     *       (e.g. set $options['file'] = '/admin/filecontainingyourclass.php';)
395
     * @return array the options needed to recreate this user_selector.
396
     */
397
    protected function get_options() {
398
        return array(
399
            'class' => get_class($this),
400
            'name' => $this->name,
401
            'exclude' => $this->exclude,
402
            'multiselect' => $this->multiselect,
403
            'accesscontext' => $this->accesscontext,
404
        );
405
    }
406
 
407
    /**
408
     * Returns true if this control is validating a list of users.
409
     *
410
     * @return boolean if true, we are validating a list of selected users,
411
     *      rather than preparing a list of uesrs to choose from.
412
     */
413
    protected function is_validating() {
414
        return !is_null($this->validatinguserids);
415
    }
416
 
417
    /**
418
     * Get the list of users that were selected by doing optional_param then validating the result.
419
     *
420
     * @return array of user objects.
421
     */
422
    protected function load_selected_users() {
423
        // See if we got anything.
424
        if ($this->multiselect) {
425
            $userids = optional_param_array($this->name, array(), PARAM_INT);
426
        } else if ($userid = optional_param($this->name, 0, PARAM_INT)) {
427
            $userids = array($userid);
428
        }
429
        // If there are no users there is nobody to load.
430
        if (empty($userids)) {
431
            return array();
432
        }
433
 
434
        // If we did, use the find_users method to validate the ids.
435
        $this->validatinguserids = $userids;
436
        $groupedusers = $this->find_users('');
437
        $this->validatinguserids = null;
438
 
439
        // Aggregate the resulting list back into a single one.
440
        $users = array();
441
        foreach ($groupedusers as $group) {
442
            foreach ($group as $user) {
443
                if (!isset($users[$user->id]) && empty($user->disabled) && in_array($user->id, $userids)) {
444
                    $users[$user->id] = $user;
445
                }
446
            }
447
        }
448
 
449
        // If we are only supposed to be selecting a single user, make sure we do.
450
        if (!$this->multiselect && count($users) > 1) {
451
            $users = array_slice($users, 0, 1);
452
        }
453
 
454
        return $users;
455
    }
456
 
457
    /**
458
     * Returns SQL to select required fields.
459
     *
460
     * @param string $u the table alias for the user table in the query being
461
     *      built. May be ''.
462
     * @return string fragment of SQL to go in the select list of the query.
463
     * @throws coding_exception if used when includecustomfields is true
464
     */
465
    protected function required_fields_sql(string $u) {
466
        if ($this->includecustomfields) {
467
            throw new coding_exception('required_fields_sql() is not needed when includecustomfields is true, '.
468
                    'use $userfieldsselects instead.');
469
        }
470
 
471
        // Raw list of fields.
472
        $fields = array('id');
473
        // Add additional name fields.
474
        $fields = array_merge($fields, \core_user\fields::get_name_fields(), $this->extrafields);
475
 
476
        // Prepend the table alias.
477
        if ($u) {
478
            foreach ($fields as &$field) {
479
                $field = $u . '.' . $field;
480
            }
481
        }
482
        return implode(',', $fields);
483
    }
484
 
485
    /**
486
     * Returns an array with SQL to perform a search and the params that go into it.
487
     *
488
     * @param string $search the text to search for.
489
     * @param string $u the table alias for the user table in the query being
490
     *      built. May be ''.
491
     * @return array an array with two elements, a fragment of SQL to go in the
492
     *      where clause the query, and an array containing any required parameters.
493
     *      this uses ? style placeholders.
494
     */
495
    protected function search_sql(string $search, string $u): array {
496
        $extrafields = $this->includecustomfields
497
            ? array_values($this->userfieldsmappings)
498
            : $this->extrafields;
499
        return users_search_sql($search, $u, $this->searchtype, $extrafields,
500
                $this->exclude, $this->validatinguserids);
501
    }
502
 
503
    /**
504
     * Used to generate a nice message when there are too many users to show.
505
     *
506
     * The message includes the number of users that currently match, and the
507
     * text of the message depends on whether the search term is non-blank.
508
     *
509
     * @param string $search the search term, as passed in to the find users method.
510
     * @param int $count the number of users that currently match.
511
     * @return array in the right format to return from the find_users method.
512
     */
513
    protected function too_many_results($search, $count) {
514
        if ($search) {
515
            $a = new stdClass;
516
            $a->count = $count;
517
            $a->search = $search;
518
            return array(get_string('toomanyusersmatchsearch', '', $a) => array(),
519
                    get_string('pleasesearchmore') => array());
520
        } else {
521
            return array(get_string('toomanyuserstoshow', '', $count) => array(),
522
                    get_string('pleaseusesearch') => array());
523
        }
524
    }
525
 
526
    /**
527
     * Output the list of <optgroup>s and <options>s that go inside the select.
528
     *
529
     * This method should do the same as the JavaScript method
530
     * user_selector.prototype.handle_response.
531
     *
532
     * @param array $groupedusers an array, as returned by find_users.
533
     * @param string $search
534
     * @return string HTML code.
535
     */
536
    protected function output_options($groupedusers, $search) {
537
        $output = '';
538
 
539
        // Ensure that the list of previously selected users is up to date.
540
        $this->get_selected_users();
541
 
542
        // If $groupedusers is empty, make a 'no matching users' group. If there is
543
        // only one selected user, set a flag to select them if that option is turned on.
544
        $select = false;
545
        if (empty($groupedusers)) {
546
            if (!empty($search)) {
547
                $groupedusers = array(get_string('nomatchingusers', '', $search) => array());
548
            } else {
549
                $groupedusers = array(get_string('none') => array());
550
            }
551
        } else if ($this->autoselectunique && count($groupedusers) == 1 &&
552
                count(reset($groupedusers)) == 1) {
553
            $select = true;
554
            if (!$this->multiselect) {
555
                $this->selected = array();
556
            }
557
        }
558
 
559
        // Output each optgroup.
560
        foreach ($groupedusers as $groupname => $users) {
561
            $output .= $this->output_optgroup($groupname, $users, $select);
562
        }
563
 
564
        // If there were previously selected users who do not match the search, show them too.
565
        if ($this->preserveselected && !empty($this->selected)) {
566
            $output .= $this->output_optgroup(get_string('previouslyselectedusers', '', $search), $this->selected, true);
567
        }
568
 
569
        // This method trashes $this->selected, so clear the cache so it is rebuilt before anyone tried to use it again.
570
        $this->selected = null;
571
 
572
        return $output;
573
    }
574
 
575
    /**
576
     * Output one particular optgroup. Used by the preceding function output_options.
577
     *
578
     * @param string $groupname the label for this optgroup.
579
     * @param array $users the users to put in this optgroup.
580
     * @param boolean $select if true, select the users in this group.
581
     * @return string HTML code.
582
     */
583
    protected function output_optgroup($groupname, $users, $select) {
584
        if (!empty($users)) {
585
            $output = '  <optgroup label="' . htmlspecialchars($groupname, ENT_COMPAT) . ' (' . count($users) . ')">' . "\n";
586
            foreach ($users as $user) {
587
                $attributes = '';
588
                if (!empty($user->disabled)) {
589
                    $attributes .= ' disabled="disabled"';
590
                } else if ($select || isset($this->selected[$user->id])) {
591
                    $attributes .= ' selected="selected"';
592
                }
593
                unset($this->selected[$user->id]);
594
                $output .= '    <option' . $attributes . ' value="' . $user->id . '">' .
595
                        $this->output_user($user) . "</option>\n";
596
                if (!empty($user->infobelow)) {
597
                    // Poor man's indent  here is because CSS styles do not work in select options, except in Firefox.
598
                    $output .= '    <option disabled="disabled" class="userselector-infobelow">' .
599
                            '&nbsp;&nbsp;&nbsp;&nbsp;' . s($user->infobelow) . '</option>';
600
                }
601
            }
602
        } else {
603
            $output = '  <optgroup label="' . htmlspecialchars($groupname, ENT_COMPAT) . '">' . "\n";
604
            $output .= '    <option disabled="disabled">&nbsp;</option>' . "\n";
605
        }
606
        $output .= "  </optgroup>\n";
607
        return $output;
608
    }
609
 
610
    /**
611
     * Convert a user object to a string suitable for displaying as an option in the list box.
612
     *
613
     * @param object $user the user to display.
614
     * @return string a string representation of the user.
615
     */
616
    public function output_user($user) {
617
        $out = fullname($user, $this->viewfullnames);
618
        if ($this->extrafields) {
619
            $displayfields = array();
620
            foreach ($this->extrafields as $field) {
621
                $displayfields[] = s($user->{$field});
622
            }
623
            $out .= ' (' . implode(', ', $displayfields) . ')';
624
        }
625
        return $out;
626
    }
627
 
628
    /**
629
     * Returns the string to use for the search button caption.
630
     *
631
     * @return string the caption for the search button.
632
     */
633
    protected function search_button_caption() {
634
        return get_string('search');
635
    }
636
 
637
    /**
638
     * Initialise one of the option checkboxes, either from  the request, or failing that from the
639
     * user_preferences table, or finally from the given default.
640
     *
641
     * @param string $name
642
     * @param mixed $default
643
     * @param string $paramtype allow the option to custom param type. default is bool
644
     * @return mixed|null|string
645
     */
646
    private function initialise_option($name, $default, $paramtype = PARAM_BOOL) {
647
        $param = optional_param($name, null, $paramtype);
648
        if (is_null($param)) {
649
            return get_user_preferences($name, $default);
650
        } else {
651
            set_user_preference($name, $param);
652
            return $param;
653
        }
654
    }
655
 
656
    /**
657
     * Output one of the options checkboxes.
658
     *
659
     * @param string $name
660
     * @param string $on
661
     * @param string $label
662
     * @return string
663
     */
664
    private function option_checkbox($name, $on, $label) {
665
        if ($on) {
666
            $checked = ' checked="checked"';
667
        } else {
668
            $checked = '';
669
        }
670
        $name = 'userselector_' . $name;
671
        // For the benefit of brain-dead IE, the id must be different from the name of the hidden form field above.
672
        // It seems that document.getElementById('frog') in IE will return and element with name="frog".
673
        $output = '<div class="form-check justify-content-start ml-1"><input type="hidden" name="' . $name . '" value="0" />' .
674
                    '<label class="form-check-label" for="' . $name . 'id">' .
675
                        '<input class="form-check-input" type="checkbox" id="' . $name . 'id" name="' . $name .
676
                            '" value="1"' . $checked . ' /> ' . $label .
677
                    "</label>
678
                   </div>\n";
679
        return $output;
680
    }
681
 
682
    /**
683
     * Get all the data for each input in the user selector search type.
684
     *
685
     * @param string $name
686
     * @param bool $checked
687
     * @param string $label
688
     * @param int $value
689
     * @param string $class
690
     * @return array a list of attributes for input.
691
     */
692
    private function get_radio_searchtype_data(string $name, bool $checked, string $label, int $value, string $class): array {
693
        $name = 'userselector_' . $name;
694
        $id = $name . 'id';
695
        $attrs = [
696
            'value' => $value,
697
            'id' => $id,
698
            'name' => $name,
699
            'class' => $class,
700
            'label' => $label,
701
        ];
702
        if ($checked) {
703
            $attrs['checked'] = true;
704
        }
705
        return $attrs;
706
    }
707
 
708
    /**
709
     * Output the search type radio buttons.
710
     *
711
     * @return string
712
     */
713
    private function output_searchtype_radios(): string {
714
        global $OUTPUT;
715
        $fields[] = $this->get_radio_searchtype_data('searchexactmatchesonly', $this->searchtype === USER_SEARCH_EXACT_MATCH,
716
            get_string('userselectorsearchexactmatchonly'), USER_SEARCH_EXACT_MATCH, 'mr-1');
717
        $fields[] = $this->get_radio_searchtype_data('searchfromstart', $this->searchtype === USER_SEARCH_STARTS_WITH,
718
            get_string('userselectorsearchfromstart'), USER_SEARCH_STARTS_WITH, 'mr-1');
719
        $fields[] = $this->get_radio_searchtype_data('searchanywhere', $this->searchtype === USER_SEARCH_CONTAINS,
720
            get_string('userselectorsearchanywhere'), USER_SEARCH_CONTAINS, 'mr-1');
721
        return $OUTPUT->render_from_template('core_user/form_user_selector_searchtype', (object) ['fields' => $fields]);
722
    }
723
 
724
    /**
725
     * Initialises JS for this control.
726
     *
727
     * @param string $search
728
     * @return string any HTML needed here.
729
     */
730
    protected function initialise_javascript($search) {
731
        global $USER, $PAGE;
732
        $output = '';
733
 
734
        // Put the options into the session, to allow search.php to respond to the ajax requests.
735
        $options = $this->get_options();
736
        $hash = md5(serialize($options));
737
        $USER->userselectors[$hash] = $options;
738
 
739
        // Initialise the selector.
740
        $PAGE->requires->js_init_call(
741
            'M.core_user.init_user_selector',
742
            array($this->name, $hash, $this->extrafields, $search, $this->searchtype),
743
            false,
744
            self::$jsmodule
745
        );
746
        return $output;
747
    }
748
}
749
 
750
/**
751
 * Base class to avoid duplicating code.
752
 *
753
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
754
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
755
 */
756
abstract class groups_user_selector_base extends user_selector_base {
757
    /** @var int */
758
    protected $groupid;
759
    /** @var  int */
760
    protected $courseid;
761
 
762
    /**
763
     * Constructor.
764
     *
765
     * @param string $name control name
766
     * @param array $options should have two elements with keys groupid and courseid.
767
     */
768
    public function __construct($name, $options) {
769
        global $CFG;
770
        $options['accesscontext'] = context_course::instance($options['courseid']);
771
        $options['includecustomfields'] = true;
772
        parent::__construct($name, $options);
773
        $this->groupid = $options['groupid'];
774
        $this->courseid = $options['courseid'];
775
        require_once($CFG->dirroot . '/group/lib.php');
776
    }
777
 
778
    /**
779
     * Returns options for this selector.
780
     * @return array
781
     */
782
    protected function get_options() {
783
        $options = parent::get_options();
784
        $options['groupid'] = $this->groupid;
785
        $options['courseid'] = $this->courseid;
786
        return $options;
787
    }
788
 
789
    /**
790
     * Creates an organised array from given data.
791
     *
792
     * @param array $roles array in the format returned by groups_calculate_role_people.
793
     * @param string $search
794
     * @return array array in the format find_users is supposed to return.
795
     */
796
    protected function convert_array_format($roles, $search) {
797
        if (empty($roles)) {
798
            $roles = array();
799
        }
800
        $groupedusers = array();
801
        foreach ($roles as $role) {
802
            if ($search) {
803
                $a = new stdClass;
804
                $a->role = html_entity_decode($role->name, ENT_QUOTES, 'UTF-8');
805
                $a->search = $search;
806
                $groupname = get_string('matchingsearchandrole', '', $a);
807
            } else {
808
                $groupname = html_entity_decode($role->name, ENT_QUOTES, 'UTF-8');
809
            }
810
            $groupedusers[$groupname] = $role->users;
811
            foreach ($groupedusers[$groupname] as &$user) {
812
                unset($user->roles);
813
                $user->fullname = fullname($user);
814
                if (!empty($user->component)) {
815
                    $user->infobelow = get_string('addedby', 'group',
816
                        get_string('pluginname', $user->component));
817
                }
818
            }
819
        }
820
        return $groupedusers;
821
    }
822
}
823
 
824
/**
825
 * User selector subclass for the list of users who are in a certain group.
826
 *
827
 * Used on the add group memebers page.
828
 *
829
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
830
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
831
 */
832
class group_members_selector extends groups_user_selector_base {
833
 
834
    /**
835
     * Finds users to display in this control.
836
     * @param string $search
837
     * @return array
838
     */
839
    public function find_users($search) {
840
        list($wherecondition, $params) = $this->search_sql($search, 'u');
841
 
842
        list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings);
843
 
844
        $roles = groups_get_members_by_role($this->groupid, $this->courseid,
845
                $this->userfieldsselects . ', gm.component',
846
                $sort, $wherecondition, array_merge($params, $sortparams, $this->userfieldsparams), $this->userfieldsjoin);
847
 
848
        return $this->convert_array_format($roles, $search);
849
    }
850
}
851
 
852
/**
853
 * User selector subclass for the list of users who are not in a certain group.
854
 *
855
 * Used on the add group members page.
856
 *
857
 * @copyright 1999 onwards Martin Dougiamas  http://dougiamas.com
858
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
859
 */
860
class group_non_members_selector extends groups_user_selector_base {
861
    /**
862
     * An array of user ids populated by find_users() used in print_user_summaries()
863
     * @var array
864
     */
865
    private $potentialmembersids = array();
866
 
867
    /**
868
     * Output user.
869
     *
870
     * @param stdClass $user
871
     * @return string
872
     */
873
    public function output_user($user) {
874
        return get_string('usergroupselectorcount', 'core_user',
875
            (object) ['groupcount' => $user->numgroups, 'fullname' => parent::output_user($user)]);
876
    }
877
 
878
    /**
879
     * Returns the user selector JavaScript module
880
     * @return array
881
     */
882
    public function get_js_module() {
883
        return self::$jsmodule;
884
    }
885
 
886
    /**
887
     * Creates a global JS variable (userSummaries) that is used by the group selector
888
     * to print related information when the user clicks on a user in the groups UI.
889
     *
890
     * Used by /group/clientlib.js
891
     *
892
     * @global moodle_page $PAGE
893
     * @param int $courseid
894
     */
895
    public function print_user_summaries($courseid) {
896
        global $PAGE;
897
        $usersummaries = $this->get_user_summaries($courseid);
898
        $PAGE->requires->data_for_js('userSummaries', $usersummaries);
899
    }
900
 
901
    /**
902
     * Construct HTML lists of group-memberships of the current set of users.
903
     *
904
     * Used in user/selector/search.php to repopulate the userSummaries JS global
905
     * that is created in self::print_user_summaries() above.
906
     *
907
     * @param int $courseid The course
908
     * @return string[] Array of HTML lists of groups.
909
     */
910
    public function get_user_summaries($courseid) {
911
        global $DB;
912
 
913
        $usersummaries = array();
914
 
915
        // Get other groups user already belongs to.
916
        $usergroups = array();
917
        $potentialmembersids = $this->potentialmembersids;
918
        if (empty($potentialmembersids) == false) {
919
            list($membersidsclause, $params) = $DB->get_in_or_equal($potentialmembersids, SQL_PARAMS_NAMED, 'pm');
920
            $sql = "SELECT u.id AS userid, g.*
921
                    FROM {user} u
922
                    JOIN {groups_members} gm ON u.id = gm.userid
923
                    JOIN {groups} g ON gm.groupid = g.id
924
                    WHERE u.id $membersidsclause AND g.courseid = :courseid ";
925
            $params['courseid'] = $courseid;
926
            $rs = $DB->get_recordset_sql($sql, $params);
927
            foreach ($rs as $usergroup) {
928
                $usergroups[$usergroup->userid][$usergroup->id] = $usergroup;
929
            }
930
            $rs->close();
931
 
932
            foreach ($potentialmembersids as $userid) {
933
                if (isset($usergroups[$userid])) {
934
                    $usergrouplist = html_writer::start_tag('ul');
935
                    foreach ($usergroups[$userid] as $groupitem) {
936
                        $usergrouplist .= html_writer::tag('li', format_string($groupitem->name));
937
                    }
938
                    $usergrouplist .= html_writer::end_tag('ul');
939
                } else {
940
                    $usergrouplist = '';
941
                }
942
                $usersummaries[] = $usergrouplist;
943
            }
944
        }
945
        return $usersummaries;
946
    }
947
 
948
    /**
949
     * Finds users to display in this control.
950
     *
951
     * @param string $search
952
     * @return array
953
     */
954
    public function find_users($search) {
955
        global $DB;
956
 
957
        // Get list of allowed roles.
958
        $context = context_course::instance($this->courseid);
959
        if ($validroleids = groups_get_possible_roles($context)) {
960
            list($roleids, $roleparams) = $DB->get_in_or_equal($validroleids, SQL_PARAMS_NAMED, 'r');
961
        } else {
962
            $roleids = " = -1";
963
            $roleparams = array();
964
        }
965
 
966
        // We want to query both the current context and parent contexts.
967
        list($relatedctxsql, $relatedctxparams) =
968
            $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx');
969
 
970
        // Get the search condition.
971
        list($searchcondition, $searchparams) = $this->search_sql($search, 'u');
972
 
973
        // Build the SQL.
974
        $enrolledjoin = get_enrolled_join($context, 'u.id');
975
 
976
        $wheres = [];
977
        $wheres[] = $enrolledjoin->wheres;
978
        $wheres[] = 'u.deleted = 0';
979
        $wheres[] = 'gm.id IS NULL';
980
        $wheres = implode(' AND ', $wheres);
981
        $wheres .= ' AND ' . $searchcondition;
982
 
983
        $fields = "SELECT r.id AS roleid, u.id AS userid,
984
                          " . $this->userfieldsselects . ",
985
                          (SELECT count(igm.groupid)
986
                             FROM {groups_members} igm
987
                             JOIN {groups} ig ON igm.groupid = ig.id
988
                            WHERE igm.userid = u.id AND ig.courseid = :courseid) AS numgroups";
989
        $sql = "   FROM {user} u
990
                   $enrolledjoin->joins
991
              LEFT JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.contextid $relatedctxsql AND ra.roleid $roleids)
992
              LEFT JOIN {role} r ON r.id = ra.roleid
993
              LEFT JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)
994
              $this->userfieldsjoin
995
                  WHERE $wheres";
996
 
997
        list($sort, $sortparams) = users_order_by_sql('u', $search, $this->accesscontext, $this->userfieldsmappings);
998
        $orderby = ' ORDER BY ' . $sort;
999
 
1000
        $params = array_merge($searchparams, $roleparams, $relatedctxparams, $enrolledjoin->params, $this->userfieldsparams);
1001
        $params['courseid'] = $this->courseid;
1002
        $params['groupid']  = $this->groupid;
1003
 
1004
        if (!$this->is_validating()) {
1005
            $potentialmemberscount = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sql", $params);
1006
            if ($potentialmemberscount > $this->maxusersperpage) {
1007
                return $this->too_many_results($search, $potentialmemberscount);
1008
            }
1009
        }
1010
 
1011
        $rs = $DB->get_recordset_sql("$fields $sql $orderby", array_merge($params, $sortparams));
1012
        $roles = groups_calculate_role_people($rs, $context);
1013
 
1014
        // Don't hold onto user IDs if we're doing validation.
1015
        if (empty($this->validatinguserids) ) {
1016
            if ($roles) {
1017
                foreach ($roles as $k => $v) {
1018
                    if ($v) {
1019
                        foreach ($v->users as $uid => $userobject) {
1020
                            $this->potentialmembersids[] = $uid;
1021
                        }
1022
                    }
1023
                }
1024
            }
1025
        }
1026
 
1027
        return $this->convert_array_format($roles, $search);
1028
    }
1029
}