Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   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 . '" ' .
 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=\"form-inline\">\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      public abstract 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  }