Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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