Search moodle.org's
Developer Documentation

See Release Notes

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

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

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