Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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