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.
/user/ -> lib.php (source)

Differences Between: [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   * External user API
  19   *
  20   * @package   core_user
  21   * @copyright 2009 Moodle Pty Ltd (http://moodle.com)
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  define('USER_FILTER_ENROLMENT', 1);
  26  define('USER_FILTER_GROUP', 2);
  27  define('USER_FILTER_LAST_ACCESS', 3);
  28  define('USER_FILTER_ROLE', 4);
  29  define('USER_FILTER_STATUS', 5);
  30  define('USER_FILTER_STRING', 6);
  31  
  32  /**
  33   * Creates a user
  34   *
  35   * @throws moodle_exception
  36   * @param stdClass $user user to create
  37   * @param bool $updatepassword if true, authentication plugin will update password.
  38   * @param bool $triggerevent set false if user_created event should not be triggred.
  39   *             This will not affect user_password_updated event triggering.
  40   * @return int id of the newly created user
  41   */
  42  function user_create_user($user, $updatepassword = true, $triggerevent = true) {
  43      global $DB;
  44  
  45      // Set the timecreate field to the current time.
  46      if (!is_object($user)) {
  47          $user = (object) $user;
  48      }
  49  
  50      // Check username.
  51      if (trim($user->username) === '') {
  52          throw new moodle_exception('invalidusernameblank');
  53      }
  54  
  55      if ($user->username !== core_text::strtolower($user->username)) {
  56          throw new moodle_exception('usernamelowercase');
  57      }
  58  
  59      if ($user->username !== core_user::clean_field($user->username, 'username')) {
  60          throw new moodle_exception('invalidusername');
  61      }
  62  
  63      // Save the password in a temp value for later.
  64      if ($updatepassword && isset($user->password)) {
  65  
  66          // Check password toward the password policy.
  67          if (!check_password_policy($user->password, $errmsg, $user)) {
  68              throw new moodle_exception($errmsg);
  69          }
  70  
  71          $userpassword = $user->password;
  72          unset($user->password);
  73      }
  74  
  75      // Apply default values for user preferences that are stored in users table.
  76      if (!isset($user->calendartype)) {
  77          $user->calendartype = core_user::get_property_default('calendartype');
  78      }
  79      if (!isset($user->maildisplay)) {
  80          $user->maildisplay = core_user::get_property_default('maildisplay');
  81      }
  82      if (!isset($user->mailformat)) {
  83          $user->mailformat = core_user::get_property_default('mailformat');
  84      }
  85      if (!isset($user->maildigest)) {
  86          $user->maildigest = core_user::get_property_default('maildigest');
  87      }
  88      if (!isset($user->autosubscribe)) {
  89          $user->autosubscribe = core_user::get_property_default('autosubscribe');
  90      }
  91      if (!isset($user->trackforums)) {
  92          $user->trackforums = core_user::get_property_default('trackforums');
  93      }
  94      if (!isset($user->lang)) {
  95          $user->lang = core_user::get_property_default('lang');
  96      }
  97  
  98      $user->timecreated = time();
  99      $user->timemodified = $user->timecreated;
 100  
 101      // Validate user data object.
 102      $uservalidation = core_user::validate($user);
 103      if ($uservalidation !== true) {
 104          foreach ($uservalidation as $field => $message) {
 105              debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
 106              $user->$field = core_user::clean_field($user->$field, $field);
 107          }
 108      }
 109  
 110      // Insert the user into the database.
 111      $newuserid = $DB->insert_record('user', $user);
 112  
 113      // Create USER context for this user.
 114      $usercontext = context_user::instance($newuserid);
 115  
 116      // Update user password if necessary.
 117      if (isset($userpassword)) {
 118          // Get full database user row, in case auth is default.
 119          $newuser = $DB->get_record('user', array('id' => $newuserid));
 120          $authplugin = get_auth_plugin($newuser->auth);
 121          $authplugin->user_update_password($newuser, $userpassword);
 122      }
 123  
 124      // Trigger event If required.
 125      if ($triggerevent) {
 126          \core\event\user_created::create_from_userid($newuserid)->trigger();
 127      }
 128  
 129      // Purge the associated caches for the current user only.
 130      $presignupcache = \cache::make('core', 'presignup');
 131      $presignupcache->purge_current_user();
 132  
 133      return $newuserid;
 134  }
 135  
 136  /**
 137   * Update a user with a user object (will compare against the ID)
 138   *
 139   * @throws moodle_exception
 140   * @param stdClass $user the user to update
 141   * @param bool $updatepassword if true, authentication plugin will update password.
 142   * @param bool $triggerevent set false if user_updated event should not be triggred.
 143   *             This will not affect user_password_updated event triggering.
 144   */
 145  function user_update_user($user, $updatepassword = true, $triggerevent = true) {
 146      global $DB;
 147  
 148      // Set the timecreate field to the current time.
 149      if (!is_object($user)) {
 150          $user = (object) $user;
 151      }
 152  
 153      // Check username.
 154      if (isset($user->username)) {
 155          if ($user->username !== core_text::strtolower($user->username)) {
 156              throw new moodle_exception('usernamelowercase');
 157          } else {
 158              if ($user->username !== core_user::clean_field($user->username, 'username')) {
 159                  throw new moodle_exception('invalidusername');
 160              }
 161          }
 162      }
 163  
 164      // Unset password here, for updating later, if password update is required.
 165      if ($updatepassword && isset($user->password)) {
 166  
 167          // Check password toward the password policy.
 168          if (!check_password_policy($user->password, $errmsg, $user)) {
 169              throw new moodle_exception($errmsg);
 170          }
 171  
 172          $passwd = $user->password;
 173          unset($user->password);
 174      }
 175  
 176      // Make sure calendartype, if set, is valid.
 177      if (empty($user->calendartype)) {
 178          // Unset this variable, must be an empty string, which we do not want to update the calendartype to.
 179          unset($user->calendartype);
 180      }
 181  
 182      $user->timemodified = time();
 183  
 184      // Validate user data object.
 185      $uservalidation = core_user::validate($user);
 186      if ($uservalidation !== true) {
 187          foreach ($uservalidation as $field => $message) {
 188              debugging("The property '$field' has invalid data and has been cleaned.", DEBUG_DEVELOPER);
 189              $user->$field = core_user::clean_field($user->$field, $field);
 190          }
 191      }
 192  
 193      $DB->update_record('user', $user);
 194  
 195      if ($updatepassword) {
 196          // Get full user record.
 197          $updateduser = $DB->get_record('user', array('id' => $user->id));
 198  
 199          // If password was set, then update its hash.
 200          if (isset($passwd)) {
 201              $authplugin = get_auth_plugin($updateduser->auth);
 202              if ($authplugin->can_change_password()) {
 203                  $authplugin->user_update_password($updateduser, $passwd);
 204              }
 205          }
 206      }
 207      // Trigger event if required.
 208      if ($triggerevent) {
 209          \core\event\user_updated::create_from_userid($user->id)->trigger();
 210      }
 211  }
 212  
 213  /**
 214   * Marks user deleted in internal user database and notifies the auth plugin.
 215   * Also unenrols user from all roles and does other cleanup.
 216   *
 217   * @todo Decide if this transaction is really needed (look for internal TODO:)
 218   * @param object $user Userobject before delete    (without system magic quotes)
 219   * @return boolean success
 220   */
 221  function user_delete_user($user) {
 222      return delete_user($user);
 223  }
 224  
 225  /**
 226   * Get users by id
 227   *
 228   * @param array $userids id of users to retrieve
 229   * @return array
 230   */
 231  function user_get_users_by_id($userids) {
 232      global $DB;
 233      return $DB->get_records_list('user', 'id', $userids);
 234  }
 235  
 236  /**
 237   * Returns the list of default 'displayable' fields
 238   *
 239   * Contains database field names but also names used to generate information, such as enrolledcourses
 240   *
 241   * @return array of user fields
 242   */
 243  function user_get_default_fields() {
 244      return array( 'id', 'username', 'fullname', 'firstname', 'lastname', 'email',
 245          'address', 'phone1', 'phone2', 'icq', 'skype', 'yahoo', 'aim', 'msn', 'department',
 246          'institution', 'interests', 'firstaccess', 'lastaccess', 'auth', 'confirmed',
 247          'idnumber', 'lang', 'theme', 'timezone', 'mailformat', 'description', 'descriptionformat',
 248          'city', 'url', 'country', 'profileimageurlsmall', 'profileimageurl', 'customfields',
 249          'groups', 'roles', 'preferences', 'enrolledcourses', 'suspended', 'lastcourseaccess'
 250      );
 251  }
 252  
 253  /**
 254   *
 255   * Give user record from mdl_user, build an array contains all user details.
 256   *
 257   * Warning: description file urls are 'webservice/pluginfile.php' is use.
 258   *          it can be changed with $CFG->moodlewstextformatlinkstoimagesfile
 259   *
 260   * @throws moodle_exception
 261   * @param stdClass $user user record from mdl_user
 262   * @param stdClass $course moodle course
 263   * @param array $userfields required fields
 264   * @return array|null
 265   */
 266  function user_get_user_details($user, $course = null, array $userfields = array()) {
 267      global $USER, $DB, $CFG, $PAGE;
 268      require_once($CFG->dirroot . "/user/profile/lib.php"); // Custom field library.
 269      require_once($CFG->dirroot . "/lib/filelib.php");      // File handling on description and friends.
 270  
 271      $defaultfields = user_get_default_fields();
 272  
 273      if (empty($userfields)) {
 274          $userfields = $defaultfields;
 275      }
 276  
 277      foreach ($userfields as $thefield) {
 278          if (!in_array($thefield, $defaultfields)) {
 279              throw new moodle_exception('invaliduserfield', 'error', '', $thefield);
 280          }
 281      }
 282  
 283      // Make sure id and fullname are included.
 284      if (!in_array('id', $userfields)) {
 285          $userfields[] = 'id';
 286      }
 287  
 288      if (!in_array('fullname', $userfields)) {
 289          $userfields[] = 'fullname';
 290      }
 291  
 292      if (!empty($course)) {
 293          $context = context_course::instance($course->id);
 294          $usercontext = context_user::instance($user->id);
 295          $canviewdetailscap = (has_capability('moodle/user:viewdetails', $context) || has_capability('moodle/user:viewdetails', $usercontext));
 296      } else {
 297          $context = context_user::instance($user->id);
 298          $usercontext = $context;
 299          $canviewdetailscap = has_capability('moodle/user:viewdetails', $usercontext);
 300      }
 301  
 302      $currentuser = ($user->id == $USER->id);
 303      $isadmin = is_siteadmin($USER);
 304  
 305      $showuseridentityfields = get_extra_user_fields($context);
 306  
 307      if (!empty($course)) {
 308          $canviewhiddenuserfields = has_capability('moodle/course:viewhiddenuserfields', $context);
 309      } else {
 310          $canviewhiddenuserfields = has_capability('moodle/user:viewhiddendetails', $context);
 311      }
 312      $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
 313      if (!empty($course)) {
 314          $canviewuseremail = has_capability('moodle/course:useremail', $context);
 315      } else {
 316          $canviewuseremail = false;
 317      }
 318      $cannotviewdescription   = !empty($CFG->profilesforenrolledusersonly) && !$currentuser && !$DB->record_exists('role_assignments', array('userid' => $user->id));
 319      if (!empty($course)) {
 320          $canaccessallgroups = has_capability('moodle/site:accessallgroups', $context);
 321      } else {
 322          $canaccessallgroups = false;
 323      }
 324  
 325      if (!$currentuser && !$canviewdetailscap && !has_coursecontact_role($user->id)) {
 326          // Skip this user details.
 327          return null;
 328      }
 329  
 330      $userdetails = array();
 331      $userdetails['id'] = $user->id;
 332  
 333      if (in_array('username', $userfields)) {
 334          if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
 335              $userdetails['username'] = $user->username;
 336          }
 337      }
 338      if ($isadmin or $canviewfullnames) {
 339          if (in_array('firstname', $userfields)) {
 340              $userdetails['firstname'] = $user->firstname;
 341          }
 342          if (in_array('lastname', $userfields)) {
 343              $userdetails['lastname'] = $user->lastname;
 344          }
 345      }
 346      $userdetails['fullname'] = fullname($user, $canviewfullnames);
 347  
 348      if (in_array('customfields', $userfields)) {
 349          $categories = profile_get_user_fields_with_data_by_category($user->id);
 350          $userdetails['customfields'] = array();
 351          foreach ($categories as $categoryid => $fields) {
 352              foreach ($fields as $formfield) {
 353                  if ($formfield->is_visible() and !$formfield->is_empty()) {
 354  
 355                      // TODO: Part of MDL-50728, this conditional coding must be moved to
 356                      // proper profile fields API so they are self-contained.
 357                      // We only use display_data in fields that require text formatting.
 358                      if ($formfield->field->datatype == 'text' or $formfield->field->datatype == 'textarea') {
 359                          $fieldvalue = $formfield->display_data();
 360                      } else {
 361                          // Cases: datetime, checkbox and menu.
 362                          $fieldvalue = $formfield->data;
 363                      }
 364  
 365                      $userdetails['customfields'][] =
 366                          array('name' => $formfield->field->name, 'value' => $fieldvalue,
 367                              'type' => $formfield->field->datatype, 'shortname' => $formfield->field->shortname);
 368                  }
 369              }
 370          }
 371          // Unset customfields if it's empty.
 372          if (empty($userdetails['customfields'])) {
 373              unset($userdetails['customfields']);
 374          }
 375      }
 376  
 377      // Profile image.
 378      if (in_array('profileimageurl', $userfields)) {
 379          $userpicture = new user_picture($user);
 380          $userpicture->size = 1; // Size f1.
 381          $userdetails['profileimageurl'] = $userpicture->get_url($PAGE)->out(false);
 382      }
 383      if (in_array('profileimageurlsmall', $userfields)) {
 384          if (!isset($userpicture)) {
 385              $userpicture = new user_picture($user);
 386          }
 387          $userpicture->size = 0; // Size f2.
 388          $userdetails['profileimageurlsmall'] = $userpicture->get_url($PAGE)->out(false);
 389      }
 390  
 391      // Hidden user field.
 392      if ($canviewhiddenuserfields) {
 393          $hiddenfields = array();
 394          // Address, phone1 and phone2 not appears in hidden fields list but require viewhiddenfields capability
 395          // according to user/profile.php.
 396          if (!empty($user->address) && in_array('address', $userfields)) {
 397              $userdetails['address'] = $user->address;
 398          }
 399      } else {
 400          $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
 401      }
 402  
 403      if (!empty($user->phone1) && in_array('phone1', $userfields) &&
 404              (in_array('phone1', $showuseridentityfields) or $canviewhiddenuserfields)) {
 405          $userdetails['phone1'] = $user->phone1;
 406      }
 407      if (!empty($user->phone2) && in_array('phone2', $userfields) &&
 408              (in_array('phone2', $showuseridentityfields) or $canviewhiddenuserfields)) {
 409          $userdetails['phone2'] = $user->phone2;
 410      }
 411  
 412      if (isset($user->description) &&
 413          ((!isset($hiddenfields['description']) && !$cannotviewdescription) or $isadmin)) {
 414          if (in_array('description', $userfields)) {
 415              // Always return the descriptionformat if description is requested.
 416              list($userdetails['description'], $userdetails['descriptionformat']) =
 417                      external_format_text($user->description, $user->descriptionformat,
 418                              $usercontext->id, 'user', 'profile', null);
 419          }
 420      }
 421  
 422      if (in_array('country', $userfields) && (!isset($hiddenfields['country']) or $isadmin) && $user->country) {
 423          $userdetails['country'] = $user->country;
 424      }
 425  
 426      if (in_array('city', $userfields) && (!isset($hiddenfields['city']) or $isadmin) && $user->city) {
 427          $userdetails['city'] = $user->city;
 428      }
 429  
 430      if (in_array('url', $userfields) && $user->url && (!isset($hiddenfields['webpage']) or $isadmin)) {
 431          $url = $user->url;
 432          if (strpos($user->url, '://') === false) {
 433              $url = 'http://'. $url;
 434          }
 435          $user->url = clean_param($user->url, PARAM_URL);
 436          $userdetails['url'] = $user->url;
 437      }
 438  
 439      if (in_array('icq', $userfields) && $user->icq && (!isset($hiddenfields['icqnumber']) or $isadmin)) {
 440          $userdetails['icq'] = $user->icq;
 441      }
 442  
 443      if (in_array('skype', $userfields) && $user->skype && (!isset($hiddenfields['skypeid']) or $isadmin)) {
 444          $userdetails['skype'] = $user->skype;
 445      }
 446      if (in_array('yahoo', $userfields) && $user->yahoo && (!isset($hiddenfields['yahooid']) or $isadmin)) {
 447          $userdetails['yahoo'] = $user->yahoo;
 448      }
 449      if (in_array('aim', $userfields) && $user->aim && (!isset($hiddenfields['aimid']) or $isadmin)) {
 450          $userdetails['aim'] = $user->aim;
 451      }
 452      if (in_array('msn', $userfields) && $user->msn && (!isset($hiddenfields['msnid']) or $isadmin)) {
 453          $userdetails['msn'] = $user->msn;
 454      }
 455      if (in_array('suspended', $userfields) && (!isset($hiddenfields['suspended']) or $isadmin)) {
 456          $userdetails['suspended'] = (bool)$user->suspended;
 457      }
 458  
 459      if (in_array('firstaccess', $userfields) && (!isset($hiddenfields['firstaccess']) or $isadmin)) {
 460          if ($user->firstaccess) {
 461              $userdetails['firstaccess'] = $user->firstaccess;
 462          } else {
 463              $userdetails['firstaccess'] = 0;
 464          }
 465      }
 466      if (in_array('lastaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
 467          if ($user->lastaccess) {
 468              $userdetails['lastaccess'] = $user->lastaccess;
 469          } else {
 470              $userdetails['lastaccess'] = 0;
 471          }
 472      }
 473  
 474      // Hidden fields restriction to lastaccess field applies to both site and course access time.
 475      if (in_array('lastcourseaccess', $userfields) && (!isset($hiddenfields['lastaccess']) or $isadmin)) {
 476          if (isset($user->lastcourseaccess)) {
 477              $userdetails['lastcourseaccess'] = $user->lastcourseaccess;
 478          } else {
 479              $userdetails['lastcourseaccess'] = 0;
 480          }
 481      }
 482  
 483      if (in_array('email', $userfields) && (
 484              $currentuser
 485              or (!isset($hiddenfields['email']) and (
 486                  $user->maildisplay == core_user::MAILDISPLAY_EVERYONE
 487                  or ($user->maildisplay == core_user::MAILDISPLAY_COURSE_MEMBERS_ONLY and enrol_sharing_course($user, $USER))
 488                  or $canviewuseremail  // TODO: Deprecate/remove for MDL-37479.
 489              ))
 490              or in_array('email', $showuseridentityfields)
 491         )) {
 492          $userdetails['email'] = $user->email;
 493      }
 494  
 495      if (in_array('interests', $userfields)) {
 496          $interests = core_tag_tag::get_item_tags_array('core', 'user', $user->id, core_tag_tag::BOTH_STANDARD_AND_NOT, 0, false);
 497          if ($interests) {
 498              $userdetails['interests'] = join(', ', $interests);
 499          }
 500      }
 501  
 502      // Departement/Institution/Idnumber are not displayed on any profile, however you can get them from editing profile.
 503      if (in_array('idnumber', $userfields) && $user->idnumber) {
 504          if (in_array('idnumber', $showuseridentityfields) or $currentuser or
 505                  has_capability('moodle/user:viewalldetails', $context)) {
 506              $userdetails['idnumber'] = $user->idnumber;
 507          }
 508      }
 509      if (in_array('institution', $userfields) && $user->institution) {
 510          if (in_array('institution', $showuseridentityfields) or $currentuser or
 511                  has_capability('moodle/user:viewalldetails', $context)) {
 512              $userdetails['institution'] = $user->institution;
 513          }
 514      }
 515      // Isset because it's ok to have department 0.
 516      if (in_array('department', $userfields) && isset($user->department)) {
 517          if (in_array('department', $showuseridentityfields) or $currentuser or
 518                  has_capability('moodle/user:viewalldetails', $context)) {
 519              $userdetails['department'] = $user->department;
 520          }
 521      }
 522  
 523      if (in_array('roles', $userfields) && !empty($course)) {
 524          // Not a big secret.
 525          $roles = get_user_roles($context, $user->id, false);
 526          $userdetails['roles'] = array();
 527          foreach ($roles as $role) {
 528              $userdetails['roles'][] = array(
 529                  'roleid'       => $role->roleid,
 530                  'name'         => $role->name,
 531                  'shortname'    => $role->shortname,
 532                  'sortorder'    => $role->sortorder
 533              );
 534          }
 535      }
 536  
 537      // If groups are in use and enforced throughout the course, then make sure we can meet in at least one course level group.
 538      if (in_array('groups', $userfields) && !empty($course) && $canaccessallgroups) {
 539          $usergroups = groups_get_all_groups($course->id, $user->id, $course->defaultgroupingid,
 540                  'g.id, g.name,g.description,g.descriptionformat');
 541          $userdetails['groups'] = array();
 542          foreach ($usergroups as $group) {
 543              list($group->description, $group->descriptionformat) =
 544                  external_format_text($group->description, $group->descriptionformat,
 545                          $context->id, 'group', 'description', $group->id);
 546              $userdetails['groups'][] = array('id' => $group->id, 'name' => $group->name,
 547                  'description' => $group->description, 'descriptionformat' => $group->descriptionformat);
 548          }
 549      }
 550      // List of courses where the user is enrolled.
 551      if (in_array('enrolledcourses', $userfields) && !isset($hiddenfields['mycourses'])) {
 552          $enrolledcourses = array();
 553          if ($mycourses = enrol_get_users_courses($user->id, true)) {
 554              foreach ($mycourses as $mycourse) {
 555                  if ($mycourse->category) {
 556                      $coursecontext = context_course::instance($mycourse->id);
 557                      $enrolledcourse = array();
 558                      $enrolledcourse['id'] = $mycourse->id;
 559                      $enrolledcourse['fullname'] = format_string($mycourse->fullname, true, array('context' => $coursecontext));
 560                      $enrolledcourse['shortname'] = format_string($mycourse->shortname, true, array('context' => $coursecontext));
 561                      $enrolledcourses[] = $enrolledcourse;
 562                  }
 563              }
 564              $userdetails['enrolledcourses'] = $enrolledcourses;
 565          }
 566      }
 567  
 568      // User preferences.
 569      if (in_array('preferences', $userfields) && $currentuser) {
 570          $preferences = array();
 571          $userpreferences = get_user_preferences();
 572          foreach ($userpreferences as $prefname => $prefvalue) {
 573              $preferences[] = array('name' => $prefname, 'value' => $prefvalue);
 574          }
 575          $userdetails['preferences'] = $preferences;
 576      }
 577  
 578      if ($currentuser or has_capability('moodle/user:viewalldetails', $context)) {
 579          $extrafields = ['auth', 'confirmed', 'lang', 'theme', 'timezone', 'mailformat'];
 580          foreach ($extrafields as $extrafield) {
 581              if (in_array($extrafield, $userfields) && isset($user->$extrafield)) {
 582                  $userdetails[$extrafield] = $user->$extrafield;
 583              }
 584          }
 585      }
 586  
 587      // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs).
 588      if (isset($userdetails['lang'])) {
 589          $userdetails['lang'] = clean_param($userdetails['lang'], PARAM_LANG);
 590      }
 591      if (isset($userdetails['theme'])) {
 592          $userdetails['theme'] = clean_param($userdetails['theme'], PARAM_THEME);
 593      }
 594  
 595      return $userdetails;
 596  }
 597  
 598  /**
 599   * Tries to obtain user details, either recurring directly to the user's system profile
 600   * or through one of the user's course enrollments (course profile).
 601   *
 602   * @param stdClass $user The user.
 603   * @return array if unsuccessful or the allowed user details.
 604   */
 605  function user_get_user_details_courses($user) {
 606      global $USER;
 607      $userdetails = null;
 608  
 609      $systemprofile = false;
 610      if (can_view_user_details_cap($user) || ($user->id == $USER->id) || has_coursecontact_role($user->id)) {
 611          $systemprofile = true;
 612      }
 613  
 614      // Try using system profile.
 615      if ($systemprofile) {
 616          $userdetails = user_get_user_details($user, null);
 617      } else {
 618          // Try through course profile.
 619          // Get the courses that the user is enrolled in (only active).
 620          $courses = enrol_get_users_courses($user->id, true);
 621          foreach ($courses as $course) {
 622              if (user_can_view_profile($user, $course)) {
 623                  $userdetails = user_get_user_details($user, $course);
 624              }
 625          }
 626      }
 627  
 628      return $userdetails;
 629  }
 630  
 631  /**
 632   * Check if $USER have the necessary capabilities to obtain user details.
 633   *
 634   * @param stdClass $user
 635   * @param stdClass $course if null then only consider system profile otherwise also consider the course's profile.
 636   * @return bool true if $USER can view user details.
 637   */
 638  function can_view_user_details_cap($user, $course = null) {
 639      // Check $USER has the capability to view the user details at user context.
 640      $usercontext = context_user::instance($user->id);
 641      $result = has_capability('moodle/user:viewdetails', $usercontext);
 642      // Otherwise can $USER see them at course context.
 643      if (!$result && !empty($course)) {
 644          $context = context_course::instance($course->id);
 645          $result = has_capability('moodle/user:viewdetails', $context);
 646      }
 647      return $result;
 648  }
 649  
 650  /**
 651   * Return a list of page types
 652   * @param string $pagetype current page type
 653   * @param stdClass $parentcontext Block's parent context
 654   * @param stdClass $currentcontext Current context of block
 655   * @return array
 656   */
 657  function user_page_type_list($pagetype, $parentcontext, $currentcontext) {
 658      return array('user-profile' => get_string('page-user-profile', 'pagetype'));
 659  }
 660  
 661  /**
 662   * Count the number of failed login attempts for the given user, since last successful login.
 663   *
 664   * @param int|stdclass $user user id or object.
 665   * @param bool $reset Resets failed login count, if set to true.
 666   *
 667   * @return int number of failed login attempts since the last successful login.
 668   */
 669  function user_count_login_failures($user, $reset = true) {
 670      global $DB;
 671  
 672      if (!is_object($user)) {
 673          $user = $DB->get_record('user', array('id' => $user), '*', MUST_EXIST);
 674      }
 675      if ($user->deleted) {
 676          // Deleted user, nothing to do.
 677          return 0;
 678      }
 679      $count = get_user_preferences('login_failed_count_since_success', 0, $user);
 680      if ($reset) {
 681          set_user_preference('login_failed_count_since_success', 0, $user);
 682      }
 683      return $count;
 684  }
 685  
 686  /**
 687   * Converts a string into a flat array of menu items, where each menu items is a
 688   * stdClass with fields type, url, title, pix, and imgsrc.
 689   *
 690   * @param string $text the menu items definition
 691   * @param moodle_page $page the current page
 692   * @return array
 693   */
 694  function user_convert_text_to_menu_items($text, $page) {
 695      global $OUTPUT, $CFG;
 696  
 697      $lines = explode("\n", $text);
 698      $items = array();
 699      $lastchild = null;
 700      $lastdepth = null;
 701      $lastsort = 0;
 702      $children = array();
 703      foreach ($lines as $line) {
 704          $line = trim($line);
 705          $bits = explode('|', $line, 3);
 706          $itemtype = 'link';
 707          if (preg_match("/^#+$/", $line)) {
 708              $itemtype = 'divider';
 709          } else if (!array_key_exists(0, $bits) or empty($bits[0])) {
 710              // Every item must have a name to be valid.
 711              continue;
 712          } else {
 713              $bits[0] = ltrim($bits[0], '-');
 714          }
 715  
 716          // Create the child.
 717          $child = new stdClass();
 718          $child->itemtype = $itemtype;
 719          if ($itemtype === 'divider') {
 720              // Add the divider to the list of children and skip link
 721              // processing.
 722              $children[] = $child;
 723              continue;
 724          }
 725  
 726          // Name processing.
 727          $namebits = explode(',', $bits[0], 2);
 728          if (count($namebits) == 2) {
 729              // Check the validity of the identifier part of the string.
 730              if (clean_param($namebits[0], PARAM_STRINGID) !== '') {
 731                  // Treat this as a language string.
 732                  $child->title = get_string($namebits[0], $namebits[1]);
 733                  $child->titleidentifier = implode(',', $namebits);
 734              }
 735          }
 736          if (empty($child->title)) {
 737              // Use it as is, don't even clean it.
 738              $child->title = $bits[0];
 739              $child->titleidentifier = str_replace(" ", "-", $bits[0]);
 740          }
 741  
 742          // URL processing.
 743          if (!array_key_exists(1, $bits) or empty($bits[1])) {
 744              // Set the url to null, and set the itemtype to invalid.
 745              $bits[1] = null;
 746              $child->itemtype = "invalid";
 747          } else {
 748              // Nasty hack to replace the grades with the direct url.
 749              if (strpos($bits[1], '/grade/report/mygrades.php') !== false) {
 750                  $bits[1] = user_mygrades_url();
 751              }
 752  
 753              // Make sure the url is a moodle url.
 754              $bits[1] = new moodle_url(trim($bits[1]));
 755          }
 756          $child->url = $bits[1];
 757  
 758          // PIX processing.
 759          $pixpath = "t/edit";
 760          if (!array_key_exists(2, $bits) or empty($bits[2])) {
 761              // Use the default.
 762              $child->pix = $pixpath;
 763          } else {
 764              // Check for the specified image existing.
 765              if (strpos($bits[2], '../') === 0) {
 766                  // The string starts with '../'.
 767                  // Strip off the first three characters - this should be the pix path.
 768                  $pixpath = substr($bits[2], 3);
 769              } else if (strpos($bits[2], '/') === false) {
 770                  // There is no / in the path. Prefix it with 't/', which is the default path.
 771                  $pixpath = "t/{$bits[2]}";
 772              } else {
 773                  // There is a '/' in the path - this is either a URL, or a standard pix path with no changes required.
 774                  $pixpath = $bits[2];
 775              }
 776              if ($page->theme->resolve_image_location($pixpath, 'moodle', true)) {
 777                  // Use the image.
 778                  $child->pix = $pixpath;
 779              } else {
 780                  // Treat it like a URL.
 781                  $child->pix = null;
 782                  $child->imgsrc = $bits[2];
 783              }
 784          }
 785  
 786          // Add this child to the list of children.
 787          $children[] = $child;
 788      }
 789      return $children;
 790  }
 791  
 792  /**
 793   * Get a list of essential user navigation items.
 794   *
 795   * @param stdclass $user user object.
 796   * @param moodle_page $page page object.
 797   * @param array $options associative array.
 798   *     options are:
 799   *     - avatarsize=35 (size of avatar image)
 800   * @return stdClass $returnobj navigation information object, where:
 801   *
 802   *      $returnobj->navitems    array    array of links where each link is a
 803   *                                       stdClass with fields url, title, and
 804   *                                       pix
 805   *      $returnobj->metadata    array    array of useful user metadata to be
 806   *                                       used when constructing navigation;
 807   *                                       fields include:
 808   *
 809   *          ROLE FIELDS
 810   *          asotherrole    bool    whether viewing as another role
 811   *          rolename       string  name of the role
 812   *
 813   *          USER FIELDS
 814   *          These fields are for the currently-logged in user, or for
 815   *          the user that the real user is currently logged in as.
 816   *
 817   *          userid         int        the id of the user in question
 818   *          userfullname   string     the user's full name
 819   *          userprofileurl moodle_url the url of the user's profile
 820   *          useravatar     string     a HTML fragment - the rendered
 821   *                                    user_picture for this user
 822   *          userloginfail  string     an error string denoting the number
 823   *                                    of login failures since last login
 824   *
 825   *          "REAL USER" FIELDS
 826   *          These fields are for when asotheruser is true, and
 827   *          correspond to the underlying "real user".
 828   *
 829   *          asotheruser        bool    whether viewing as another user
 830   *          realuserid         int        the id of the user in question
 831   *          realuserfullname   string     the user's full name
 832   *          realuserprofileurl moodle_url the url of the user's profile
 833   *          realuseravatar     string     a HTML fragment - the rendered
 834   *                                        user_picture for this user
 835   *
 836   *          MNET PROVIDER FIELDS
 837   *          asmnetuser            bool   whether viewing as a user from an
 838   *                                       MNet provider
 839   *          mnetidprovidername    string name of the MNet provider
 840   *          mnetidproviderwwwroot string URL of the MNet provider
 841   */
 842  function user_get_user_navigation_info($user, $page, $options = array()) {
 843      global $OUTPUT, $DB, $SESSION, $CFG;
 844  
 845      $returnobject = new stdClass();
 846      $returnobject->navitems = array();
 847      $returnobject->metadata = array();
 848  
 849      $course = $page->course;
 850  
 851      // Query the environment.
 852      $context = context_course::instance($course->id);
 853  
 854      // Get basic user metadata.
 855      $returnobject->metadata['userid'] = $user->id;
 856      $returnobject->metadata['userfullname'] = fullname($user);
 857      $returnobject->metadata['userprofileurl'] = new moodle_url('/user/profile.php', array(
 858          'id' => $user->id
 859      ));
 860  
 861      $avataroptions = array('link' => false, 'visibletoscreenreaders' => false);
 862      if (!empty($options['avatarsize'])) {
 863          $avataroptions['size'] = $options['avatarsize'];
 864      }
 865      $returnobject->metadata['useravatar'] = $OUTPUT->user_picture (
 866          $user, $avataroptions
 867      );
 868      // Build a list of items for a regular user.
 869  
 870      // Query MNet status.
 871      if ($returnobject->metadata['asmnetuser'] = is_mnet_remote_user($user)) {
 872          $mnetidprovider = $DB->get_record('mnet_host', array('id' => $user->mnethostid));
 873          $returnobject->metadata['mnetidprovidername'] = $mnetidprovider->name;
 874          $returnobject->metadata['mnetidproviderwwwroot'] = $mnetidprovider->wwwroot;
 875      }
 876  
 877      // Did the user just log in?
 878      if (isset($SESSION->justloggedin)) {
 879          // Don't unset this flag as login_info still needs it.
 880          if (!empty($CFG->displayloginfailures)) {
 881              // Don't reset the count either, as login_info() still needs it too.
 882              if ($count = user_count_login_failures($user, false)) {
 883  
 884                  // Get login failures string.
 885                  $a = new stdClass();
 886                  $a->attempts = html_writer::tag('span', $count, array('class' => 'value'));
 887                  $returnobject->metadata['userloginfail'] =
 888                      get_string('failedloginattempts', '', $a);
 889  
 890              }
 891          }
 892      }
 893  
 894      // Links: Dashboard.
 895      $myhome = new stdClass();
 896      $myhome->itemtype = 'link';
 897      $myhome->url = new moodle_url('/my/');
 898      $myhome->title = get_string('mymoodle', 'admin');
 899      $myhome->titleidentifier = 'mymoodle,admin';
 900      $myhome->pix = "i/dashboard";
 901      $returnobject->navitems[] = $myhome;
 902  
 903      // Links: My Profile.
 904      $myprofile = new stdClass();
 905      $myprofile->itemtype = 'link';
 906      $myprofile->url = new moodle_url('/user/profile.php', array('id' => $user->id));
 907      $myprofile->title = get_string('profile');
 908      $myprofile->titleidentifier = 'profile,moodle';
 909      $myprofile->pix = "i/user";
 910      $returnobject->navitems[] = $myprofile;
 911  
 912      $returnobject->metadata['asotherrole'] = false;
 913  
 914      // Before we add the last items (usually a logout + switch role link), add any
 915      // custom-defined items.
 916      $customitems = user_convert_text_to_menu_items($CFG->customusermenuitems, $page);
 917      foreach ($customitems as $item) {
 918          $returnobject->navitems[] = $item;
 919      }
 920  
 921  
 922      if ($returnobject->metadata['asotheruser'] = \core\session\manager::is_loggedinas()) {
 923          $realuser = \core\session\manager::get_realuser();
 924  
 925          // Save values for the real user, as $user will be full of data for the
 926          // user the user is disguised as.
 927          $returnobject->metadata['realuserid'] = $realuser->id;
 928          $returnobject->metadata['realuserfullname'] = fullname($realuser);
 929          $returnobject->metadata['realuserprofileurl'] = new moodle_url('/user/profile.php', array(
 930              'id' => $realuser->id
 931          ));
 932          $returnobject->metadata['realuseravatar'] = $OUTPUT->user_picture($realuser, $avataroptions);
 933  
 934          // Build a user-revert link.
 935          $userrevert = new stdClass();
 936          $userrevert->itemtype = 'link';
 937          $userrevert->url = new moodle_url('/course/loginas.php', array(
 938              'id' => $course->id,
 939              'sesskey' => sesskey()
 940          ));
 941          $userrevert->pix = "a/logout";
 942          $userrevert->title = get_string('logout');
 943          $userrevert->titleidentifier = 'logout,moodle';
 944          $returnobject->navitems[] = $userrevert;
 945  
 946      } else {
 947  
 948          // Build a logout link.
 949          $logout = new stdClass();
 950          $logout->itemtype = 'link';
 951          $logout->url = new moodle_url('/login/logout.php', array('sesskey' => sesskey()));
 952          $logout->pix = "a/logout";
 953          $logout->title = get_string('logout');
 954          $logout->titleidentifier = 'logout,moodle';
 955          $returnobject->navitems[] = $logout;
 956      }
 957  
 958      if (is_role_switched($course->id)) {
 959          if ($role = $DB->get_record('role', array('id' => $user->access['rsw'][$context->path]))) {
 960              // Build role-return link instead of logout link.
 961              $rolereturn = new stdClass();
 962              $rolereturn->itemtype = 'link';
 963              $rolereturn->url = new moodle_url('/course/switchrole.php', array(
 964                  'id' => $course->id,
 965                  'sesskey' => sesskey(),
 966                  'switchrole' => 0,
 967                  'returnurl' => $page->url->out_as_local_url(false)
 968              ));
 969              $rolereturn->pix = "a/logout";
 970              $rolereturn->title = get_string('switchrolereturn');
 971              $rolereturn->titleidentifier = 'switchrolereturn,moodle';
 972              $returnobject->navitems[] = $rolereturn;
 973  
 974              $returnobject->metadata['asotherrole'] = true;
 975              $returnobject->metadata['rolename'] = role_get_name($role, $context);
 976  
 977          }
 978      } else {
 979          // Build switch role link.
 980          $roles = get_switchable_roles($context);
 981          if (is_array($roles) && (count($roles) > 0)) {
 982              $switchrole = new stdClass();
 983              $switchrole->itemtype = 'link';
 984              $switchrole->url = new moodle_url('/course/switchrole.php', array(
 985                  'id' => $course->id,
 986                  'switchrole' => -1,
 987                  'returnurl' => $page->url->out_as_local_url(false)
 988              ));
 989              $switchrole->pix = "i/switchrole";
 990              $switchrole->title = get_string('switchroleto');
 991              $switchrole->titleidentifier = 'switchroleto,moodle';
 992              $returnobject->navitems[] = $switchrole;
 993          }
 994      }
 995  
 996      return $returnobject;
 997  }
 998  
 999  /**
1000   * Add password to the list of used hashes for this user.
1001   *
1002   * This is supposed to be used from:
1003   *  1/ change own password form
1004   *  2/ password reset process
1005   *  3/ user signup in auth plugins if password changing supported
1006   *
1007   * @param int $userid user id
1008   * @param string $password plaintext password
1009   * @return void
1010   */
1011  function user_add_password_history($userid, $password) {
1012      global $CFG, $DB;
1013  
1014      if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1015          return;
1016      }
1017  
1018      // Note: this is using separate code form normal password hashing because
1019      //       we need to have this under control in the future. Also the auth
1020      //       plugin might not store the passwords locally at all.
1021  
1022      $record = new stdClass();
1023      $record->userid = $userid;
1024      $record->hash = password_hash($password, PASSWORD_DEFAULT);
1025      $record->timecreated = time();
1026      $DB->insert_record('user_password_history', $record);
1027  
1028      $i = 0;
1029      $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1030      foreach ($records as $record) {
1031          $i++;
1032          if ($i > $CFG->passwordreuselimit) {
1033              $DB->delete_records('user_password_history', array('id' => $record->id));
1034          }
1035      }
1036  }
1037  
1038  /**
1039   * Was this password used before on change or reset password page?
1040   *
1041   * The $CFG->passwordreuselimit setting determines
1042   * how many times different password needs to be used
1043   * before allowing previously used password again.
1044   *
1045   * @param int $userid user id
1046   * @param string $password plaintext password
1047   * @return bool true if password reused
1048   */
1049  function user_is_previously_used_password($userid, $password) {
1050      global $CFG, $DB;
1051  
1052      if (empty($CFG->passwordreuselimit) or $CFG->passwordreuselimit < 0) {
1053          return false;
1054      }
1055  
1056      $reused = false;
1057  
1058      $i = 0;
1059      $records = $DB->get_records('user_password_history', array('userid' => $userid), 'timecreated DESC, id DESC');
1060      foreach ($records as $record) {
1061          $i++;
1062          if ($i > $CFG->passwordreuselimit) {
1063              $DB->delete_records('user_password_history', array('id' => $record->id));
1064              continue;
1065          }
1066          // NOTE: this is slow but we cannot compare the hashes directly any more.
1067          if (password_verify($password, $record->hash)) {
1068              $reused = true;
1069          }
1070      }
1071  
1072      return $reused;
1073  }
1074  
1075  /**
1076   * Remove a user device from the Moodle database (for PUSH notifications usually).
1077   *
1078   * @param string $uuid The device UUID.
1079   * @param string $appid The app id. If empty all the devices matching the UUID for the user will be removed.
1080   * @return bool true if removed, false if the device didn't exists in the database
1081   * @since Moodle 2.9
1082   */
1083  function user_remove_user_device($uuid, $appid = "") {
1084      global $DB, $USER;
1085  
1086      $conditions = array('uuid' => $uuid, 'userid' => $USER->id);
1087      if (!empty($appid)) {
1088          $conditions['appid'] = $appid;
1089      }
1090  
1091      if (!$DB->count_records('user_devices', $conditions)) {
1092          return false;
1093      }
1094  
1095      $DB->delete_records('user_devices', $conditions);
1096  
1097      return true;
1098  }
1099  
1100  /**
1101   * Trigger user_list_viewed event.
1102   *
1103   * @param stdClass  $course course  object
1104   * @param stdClass  $context course context object
1105   * @since Moodle 2.9
1106   */
1107  function user_list_view($course, $context) {
1108  
1109      $event = \core\event\user_list_viewed::create(array(
1110          'objectid' => $course->id,
1111          'courseid' => $course->id,
1112          'context' => $context,
1113          'other' => array(
1114              'courseshortname' => $course->shortname,
1115              'coursefullname' => $course->fullname
1116          )
1117      ));
1118      $event->trigger();
1119  }
1120  
1121  /**
1122   * Returns the url to use for the "Grades" link in the user navigation.
1123   *
1124   * @param int $userid The user's ID.
1125   * @param int $courseid The course ID if available.
1126   * @return mixed A URL to be directed to for "Grades".
1127   */
1128  function user_mygrades_url($userid = null, $courseid = SITEID) {
1129      global $CFG, $USER;
1130      $url = null;
1131      if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report != 'external') {
1132          if (isset($userid) && $USER->id != $userid) {
1133              // Send to the gradebook report.
1134              $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php',
1135                      array('id' => $courseid, 'userid' => $userid));
1136          } else {
1137              $url = new moodle_url('/grade/report/' . $CFG->grade_mygrades_report . '/index.php');
1138          }
1139      } else if (isset($CFG->grade_mygrades_report) && $CFG->grade_mygrades_report == 'external'
1140              && !empty($CFG->gradereport_mygradeurl)) {
1141          $url = $CFG->gradereport_mygradeurl;
1142      } else {
1143          $url = $CFG->wwwroot;
1144      }
1145      return $url;
1146  }
1147  
1148  /**
1149   * Check if the current user has permission to view details of the supplied user.
1150   *
1151   * This function supports two modes:
1152   * If the optional $course param is omitted, then this function finds all shared courses and checks whether the current user has
1153   * permission in any of them, returning true if so.
1154   * If the $course param is provided, then this function checks permissions in ONLY that course.
1155   *
1156   * @param object $user The other user's details.
1157   * @param object $course if provided, only check permissions in this course.
1158   * @param context $usercontext The user context if available.
1159   * @return bool true for ability to view this user, else false.
1160   */
1161  function user_can_view_profile($user, $course = null, $usercontext = null) {
1162      global $USER, $CFG;
1163  
1164      if ($user->deleted) {
1165          return false;
1166      }
1167  
1168      // Do we need to be logged in?
1169      if (empty($CFG->forceloginforprofiles)) {
1170          return true;
1171      } else {
1172         if (!isloggedin() || isguestuser()) {
1173              // User is not logged in and forceloginforprofile is set, we need to return now.
1174              return false;
1175          }
1176      }
1177  
1178      // Current user can always view their profile.
1179      if ($USER->id == $user->id) {
1180          return true;
1181      }
1182  
1183      // Use callbacks so that (primarily) local plugins can prevent or allow profile access.
1184      $forceallow = false;
1185      $plugintypes = get_plugins_with_function('control_view_profile');
1186      foreach ($plugintypes as $plugins) {
1187          foreach ($plugins as $pluginfunction) {
1188              $result = $pluginfunction($user, $course, $usercontext);
1189              switch ($result) {
1190                  case core_user::VIEWPROFILE_DO_NOT_PREVENT:
1191                      // If the plugin doesn't stop access, just continue to next plugin or use
1192                      // default behaviour.
1193                      break;
1194                  case core_user::VIEWPROFILE_FORCE_ALLOW:
1195                      // Record that we are definitely going to allow it (unless another plugin
1196                      // returns _PREVENT).
1197                      $forceallow = true;
1198                      break;
1199                  case core_user::VIEWPROFILE_PREVENT:
1200                      // If any plugin returns PREVENT then we return false, regardless of what
1201                      // other plugins said.
1202                      return false;
1203              }
1204          }
1205      }
1206      if ($forceallow) {
1207          return true;
1208      }
1209  
1210      // Course contacts have visible profiles always.
1211      if (has_coursecontact_role($user->id)) {
1212          return true;
1213      }
1214  
1215      // If we're only checking the capabilities in the single provided course.
1216      if (isset($course)) {
1217          // Confirm that $user is enrolled in the $course we're checking.
1218          if (is_enrolled(context_course::instance($course->id), $user)) {
1219              $userscourses = array($course);
1220          }
1221      } else {
1222          // Else we're checking whether the current user can view $user's profile anywhere, so check user context first.
1223          if (empty($usercontext)) {
1224              $usercontext = context_user::instance($user->id);
1225          }
1226          if (has_capability('moodle/user:viewdetails', $usercontext) || has_capability('moodle/user:viewalldetails', $usercontext)) {
1227              return true;
1228          }
1229          // This returns context information, so we can preload below.
1230          $userscourses = enrol_get_all_users_courses($user->id);
1231      }
1232  
1233      if (empty($userscourses)) {
1234          return false;
1235      }
1236  
1237      foreach ($userscourses as $userscourse) {
1238          context_helper::preload_from_record($userscourse);
1239          $coursecontext = context_course::instance($userscourse->id);
1240          if (has_capability('moodle/user:viewdetails', $coursecontext) ||
1241              has_capability('moodle/user:viewalldetails', $coursecontext)) {
1242              if (!groups_user_groups_visible($userscourse, $user->id)) {
1243                  // Not a member of the same group.
1244                  continue;
1245              }
1246              return true;
1247          }
1248      }
1249      return false;
1250  }
1251  
1252  /**
1253   * Returns users tagged with a specified tag.
1254   *
1255   * @param core_tag_tag $tag
1256   * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
1257   *             are displayed on the page and the per-page limit may be bigger
1258   * @param int $fromctx context id where the link was displayed, may be used by callbacks
1259   *            to display items in the same context first
1260   * @param int $ctx context id where to search for records
1261   * @param bool $rec search in subcontexts as well
1262   * @param int $page 0-based number of page being displayed
1263   * @return \core_tag\output\tagindex
1264   */
1265  function user_get_tagged_users($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
1266      global $PAGE;
1267  
1268      if ($ctx && $ctx != context_system::instance()->id) {
1269          $usercount = 0;
1270      } else {
1271          // Users can only be displayed in system context.
1272          $usercount = $tag->count_tagged_items('core', 'user',
1273                  'it.deleted=:notdeleted', array('notdeleted' => 0));
1274      }
1275      $perpage = $exclusivemode ? 24 : 5;
1276      $content = '';
1277      $totalpages = ceil($usercount / $perpage);
1278  
1279      if ($usercount) {
1280          $userlist = $tag->get_tagged_items('core', 'user', $page * $perpage, $perpage,
1281                  'it.deleted=:notdeleted', array('notdeleted' => 0));
1282          $renderer = $PAGE->get_renderer('core', 'user');
1283          $content .= $renderer->user_list($userlist, $exclusivemode);
1284      }
1285  
1286      return new core_tag\output\tagindex($tag, 'core', 'user', $content,
1287              $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
1288  }
1289  
1290  /**
1291   * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access a course.
1292   *
1293   * @param int $accesssince The unix timestamp to compare to users' last access
1294   * @param string $tableprefix
1295   * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1296   * @return string
1297   */
1298  function user_get_course_lastaccess_sql($accesssince = null, $tableprefix = 'ul', $haveaccessed = false) {
1299      return user_get_lastaccess_sql('timeaccess', $accesssince, $tableprefix, $haveaccessed);
1300  }
1301  
1302  /**
1303   * Returns SQL that can be used to limit a query to a period where the user last accessed / did not access the system.
1304   *
1305   * @param int $accesssince The unix timestamp to compare to users' last access
1306   * @param string $tableprefix
1307   * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1308   * @return string
1309   */
1310  function user_get_user_lastaccess_sql($accesssince = null, $tableprefix = 'u', $haveaccessed = false) {
1311      return user_get_lastaccess_sql('lastaccess', $accesssince, $tableprefix, $haveaccessed);
1312  }
1313  
1314  /**
1315   * Returns SQL that can be used to limit a query to a period where the user last accessed or
1316   * did not access something recorded by a given table.
1317   *
1318   * @param string $columnname The name of the access column to check against
1319   * @param int $accesssince The unix timestamp to compare to users' last access
1320   * @param string $tableprefix The query prefix of the table to check
1321   * @param bool $haveaccessed Whether to match against users who HAVE accessed since $accesssince (optional)
1322   * @return string
1323   */
1324  function user_get_lastaccess_sql($columnname, $accesssince, $tableprefix, $haveaccessed = false) {
1325      if (empty($accesssince)) {
1326          return '';
1327      }
1328  
1329      // Only users who have accessed since $accesssince.
1330      if ($haveaccessed) {
1331          if ($accesssince == -1) {
1332              // Include all users who have logged in at some point.
1333              $sql = "({$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0)";
1334          } else {
1335              // Users who have accessed since the specified time.
1336              $sql = "{$tableprefix}.{$columnname} IS NOT NULL AND {$tableprefix}.{$columnname} != 0
1337                  AND {$tableprefix}.{$columnname} >= {$accesssince}";
1338          }
1339      } else {
1340          // Only users who have not accessed since $accesssince.
1341  
1342          if ($accesssince == -1) {
1343              // Users who have never accessed.
1344              $sql = "({$tableprefix}.{$columnname} IS NULL OR {$tableprefix}.{$columnname} = 0)";
1345          } else {
1346              // Users who have not accessed since the specified time.
1347              $sql = "({$tableprefix}.{$columnname} IS NULL
1348                      OR ({$tableprefix}.{$columnname} != 0 AND {$tableprefix}.{$columnname} < {$accesssince}))";
1349          }
1350      }
1351  
1352      return $sql;
1353  }
1354  
1355  /**
1356   * Callback for inplace editable API.
1357   *
1358   * @param string $itemtype - Only user_roles is supported.
1359   * @param string $itemid - Courseid and userid separated by a :
1360   * @param string $newvalue - json encoded list of roleids.
1361   * @return \core\output\inplace_editable
1362   */
1363  function core_user_inplace_editable($itemtype, $itemid, $newvalue) {
1364      if ($itemtype === 'user_roles') {
1365          return \core_user\output\user_roles_editable::update($itemid, $newvalue);
1366      }
1367  }
1368  
1369  /**
1370   * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes"
1371   *
1372   * @param integer $userid
1373   * @param string $fieldname
1374   * @return string $purpose (empty string if there is no mapping).
1375   */
1376  function user_edit_map_field_purpose($userid, $fieldname) {
1377      global $USER;
1378  
1379      $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas();
1380      // These are the fields considered valid to map and auto fill from a browser.
1381      // We do not include fields that are in a collapsed section by default because
1382      // the browser could auto-fill the field and cause a new value to be saved when
1383      // that field was never visible.
1384      $validmappings = array(
1385          'username' => 'username',
1386          'password' => 'current-password',
1387          'firstname' => 'given-name',
1388          'lastname' => 'family-name',
1389          'middlename' => 'additional-name',
1390          'email' => 'email',
1391          'country' => 'country',
1392          'lang' => 'language'
1393      );
1394  
1395      $purpose = '';
1396      // Only set a purpose when editing your own user details.
1397      if ($currentuser && isset($validmappings[$fieldname])) {
1398          $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" ';
1399      }
1400  
1401      return $purpose;
1402  }
1403