Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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