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.

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   * User class
  19   *
  20   * @package    core
  21   * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  /**
  28   * User class to access user details.
  29   *
  30   * @todo       move api's from user/lib.php and deprecate old ones.
  31   * @package    core
  32   * @copyright  2013 Rajesh Taneja <rajesh@moodle.com>
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class core_user {
  36      /**
  37       * No reply user id.
  38       */
  39      const NOREPLY_USER = -10;
  40  
  41      /**
  42       * Support user id.
  43       */
  44      const SUPPORT_USER = -20;
  45  
  46      /**
  47       * Hide email address from everyone.
  48       */
  49      const MAILDISPLAY_HIDE = 0;
  50  
  51      /**
  52       * Display email address to everyone.
  53       */
  54      const MAILDISPLAY_EVERYONE = 1;
  55  
  56      /**
  57       * Display email address to course members only.
  58       */
  59      const MAILDISPLAY_COURSE_MEMBERS_ONLY = 2;
  60  
  61      /**
  62       * List of fields that can be synched/locked during authentication.
  63       */
  64      const AUTHSYNCFIELDS = [
  65          'firstname',
  66          'lastname',
  67          'email',
  68          'city',
  69          'country',
  70          'lang',
  71          'description',
  72          'idnumber',
  73          'institution',
  74          'department',
  75          'phone1',
  76          'phone2',
  77          'address',
  78          'firstnamephonetic',
  79          'lastnamephonetic',
  80          'middlename',
  81          'alternatename'
  82      ];
  83  
  84      /** @var int Indicates that user profile view should be prevented */
  85      const VIEWPROFILE_PREVENT = -1;
  86      /** @var int Indicates that user profile view should not be prevented */
  87      const VIEWPROFILE_DO_NOT_PREVENT = 0;
  88      /** @var int Indicates that user profile view should be allowed even if Moodle would prevent it */
  89      const VIEWPROFILE_FORCE_ALLOW = 1;
  90  
  91      /** @var stdClass keep record of noreply user */
  92      public static $noreplyuser = false;
  93  
  94      /** @var stdClass keep record of support user */
  95      public static $supportuser = false;
  96  
  97      /** @var array store user fields properties cache. */
  98      protected static $propertiescache = null;
  99  
 100      /** @var array store user preferences cache. */
 101      protected static $preferencescache = null;
 102  
 103      /**
 104       * Return user object from db or create noreply or support user,
 105       * if userid matches corse_user::NOREPLY_USER or corse_user::SUPPORT_USER
 106       * respectively. If userid is not found, then return false.
 107       *
 108       * @param int $userid user id
 109       * @param string $fields A comma separated list of user fields to be returned, support and noreply user
 110       *                       will not be filtered by this.
 111       * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
 112       *                        IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
 113       *                        MUST_EXIST means throw an exception if no user record or multiple records found.
 114       * @return stdClass|bool user record if found, else false.
 115       * @throws dml_exception if user record not found and respective $strictness is set.
 116       */
 117      public static function get_user($userid, $fields = '*', $strictness = IGNORE_MISSING) {
 118          global $DB;
 119  
 120          // If noreply user then create fake record and return.
 121          switch ($userid) {
 122              case self::NOREPLY_USER:
 123                  return self::get_noreply_user();
 124                  break;
 125              case self::SUPPORT_USER:
 126                  return self::get_support_user();
 127                  break;
 128              default:
 129                  return $DB->get_record('user', array('id' => $userid), $fields, $strictness);
 130          }
 131      }
 132  
 133      /**
 134       * Return user object from db based on their email.
 135       *
 136       * @param string $email The email of the user searched.
 137       * @param string $fields A comma separated list of user fields to be returned, support and noreply user.
 138       * @param int $mnethostid The id of the remote host.
 139       * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
 140       *                        IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
 141       *                        MUST_EXIST means throw an exception if no user record or multiple records found.
 142       * @return stdClass|bool user record if found, else false.
 143       * @throws dml_exception if user record not found and respective $strictness is set.
 144       */
 145      public static function get_user_by_email($email, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) {
 146          global $DB, $CFG;
 147  
 148          // Because we use the username as the search criteria, we must also restrict our search based on mnet host.
 149          if (empty($mnethostid)) {
 150              // If empty, we restrict to local users.
 151              $mnethostid = $CFG->mnet_localhost_id;
 152          }
 153  
 154          return $DB->get_record('user', array('email' => $email, 'mnethostid' => $mnethostid), $fields, $strictness);
 155      }
 156  
 157      /**
 158       * Return user object from db based on their username.
 159       *
 160       * @param string $username The username of the user searched.
 161       * @param string $fields A comma separated list of user fields to be returned, support and noreply user.
 162       * @param int $mnethostid The id of the remote host.
 163       * @param int $strictness IGNORE_MISSING means compatible mode, false returned if user not found, debug message if more found;
 164       *                        IGNORE_MULTIPLE means return first user, ignore multiple user records found(not recommended);
 165       *                        MUST_EXIST means throw an exception if no user record or multiple records found.
 166       * @return stdClass|bool user record if found, else false.
 167       * @throws dml_exception if user record not found and respective $strictness is set.
 168       */
 169      public static function get_user_by_username($username, $fields = '*', $mnethostid = null, $strictness = IGNORE_MISSING) {
 170          global $DB, $CFG;
 171  
 172          // Because we use the username as the search criteria, we must also restrict our search based on mnet host.
 173          if (empty($mnethostid)) {
 174              // If empty, we restrict to local users.
 175              $mnethostid = $CFG->mnet_localhost_id;
 176          }
 177  
 178          return $DB->get_record('user', array('username' => $username, 'mnethostid' => $mnethostid), $fields, $strictness);
 179      }
 180  
 181      /**
 182       * Searches for users by name, possibly within a specified context, with current user's access.
 183       *
 184       * Deciding which users to search is complicated because it relies on user permissions;
 185       * ideally, we shouldn't show names if you aren't allowed to see their profile. The permissions
 186       * for seeing profile are really complicated.
 187       *
 188       * Even if search is restricted to a course, it's possible that other people might have
 189       * been able to contribute within the course (e.g. they were enrolled before and not now;
 190       * or people with system-level roles) so if the user has permission we do want to include
 191       * everyone. However, if there are multiple results then we prioritise the ones who are
 192       * enrolled in the course.
 193       *
 194       * If you have moodle/user:viewdetails at system level, you can search everyone.
 195       * Otherwise we check which courses you *do* have that permission and search everyone who is
 196       * enrolled on those courses.
 197       *
 198       * Normally you can only search the user's name. If you have the moodle/site:viewuseridentity
 199       * capability then we also let you search the fields which are listed as identity fields in
 200       * the 'showuseridentity' config option. For example, this might include the user's ID number
 201       * or email.
 202       *
 203       * The $max parameter controls the maximum number of users returned. If users are restricted
 204       * from view for some reason, multiple runs of the main query might be made; the $querylimit
 205       * parameter allows this to be restricted. Both parameters can be zero to remove limits.
 206       *
 207       * The returned user objects include id, username, all fields required for user pictures, and
 208       * user identity fields.
 209       *
 210       * @param string $query Search query text
 211       * @param \context_course|null $coursecontext Course context or null if system-wide
 212       * @param int $max Max number of users to return, default 30 (zero = no limit)
 213       * @param int $querylimit Max number of database queries, default 5 (zero = no limit)
 214       * @return array Array of user objects with limited fields
 215       */
 216      public static function search($query, \context_course $coursecontext = null,
 217              $max = 30, $querylimit = 5) {
 218          global $CFG, $DB;
 219          require_once($CFG->dirroot . '/user/lib.php');
 220  
 221          // Allow limits to be turned off.
 222          if (!$max) {
 223              $max = PHP_INT_MAX;
 224          }
 225          if (!$querylimit) {
 226              $querylimit = PHP_INT_MAX;
 227          }
 228  
 229          // Check permission to view profiles at each context.
 230          $systemcontext = \context_system::instance();
 231          $viewsystem = has_capability('moodle/user:viewdetails', $systemcontext);
 232          if ($viewsystem) {
 233              $userquery = 'SELECT id FROM {user}';
 234              $userparams = [];
 235          }
 236          if (!$viewsystem) {
 237              list($userquery, $userparams) = self::get_enrolled_sql_on_courses_with_capability(
 238                      'moodle/user:viewdetails');
 239              if (!$userquery) {
 240                  // No permissions anywhere, return nothing.
 241                  return [];
 242              }
 243          }
 244  
 245          // Start building the WHERE clause based on name.
 246          list ($where, $whereparams) = users_search_sql($query, 'u');
 247  
 248          // We allow users to search with extra identity fields (as well as name) but only if they
 249          // have the permission to display those identity fields.
 250          $extrasql = '';
 251          $extraparams = [];
 252  
 253          // TODO Does not support custom user profile fields (MDL-70456).
 254          $userfieldsapi = \core_user\fields::for_identity(null, false)->with_userpic()->with_name()
 255              ->including('username', 'deleted');
 256          $selectfields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
 257          $extra = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
 258  
 259          $index = 1;
 260          foreach ($extra as $fieldname) {
 261              if ($extrasql) {
 262                  $extrasql .= ' OR ';
 263              }
 264              $extrasql .= $DB->sql_like('u.' . $fieldname, ':extra' . $index, false);
 265              $extraparams['extra' . $index] = $query . '%';
 266              $index++;
 267          }
 268  
 269          $identitysystem = has_capability('moodle/site:viewuseridentity', $systemcontext);
 270          $usingshowidentity = false;
 271          if ($identitysystem) {
 272              // They have permission everywhere so just add the extra query to the normal query.
 273              $where .= ' OR ' . $extrasql;
 274              $whereparams = array_merge($whereparams, $extraparams);
 275          } else {
 276              // Get all courses where user can view full user identity.
 277              list($sql, $params) = self::get_enrolled_sql_on_courses_with_capability(
 278                      'moodle/site:viewuseridentity');
 279              if ($sql) {
 280                  // Join that with the user query to get an extra field indicating if we can.
 281                  $userquery = "
 282                          SELECT innerusers.id, COUNT(identityusers.id) AS showidentity
 283                            FROM ($userquery) innerusers
 284                       LEFT JOIN ($sql) identityusers ON identityusers.id = innerusers.id
 285                        GROUP BY innerusers.id";
 286                  $userparams = array_merge($userparams, $params);
 287                  $usingshowidentity = true;
 288  
 289                  // Query on the extra fields only in those places.
 290                  $where .= ' OR (users.showidentity > 0 AND (' . $extrasql . '))';
 291                  $whereparams = array_merge($whereparams, $extraparams);
 292              }
 293          }
 294  
 295          // Default order is just name order. But if searching within a course then we show users
 296          // within the course first.
 297          list ($order, $orderparams) = users_order_by_sql('u', $query, $systemcontext);
 298          if ($coursecontext) {
 299              list ($sql, $params) = get_enrolled_sql($coursecontext);
 300              $mainfield = 'innerusers2.id';
 301              if ($usingshowidentity) {
 302                  $mainfield .= ', innerusers2.showidentity';
 303              }
 304              $userquery = "
 305                      SELECT $mainfield, COUNT(courseusers.id) AS incourse
 306                        FROM ($userquery) innerusers2
 307                   LEFT JOIN ($sql) courseusers ON courseusers.id = innerusers2.id
 308                    GROUP BY $mainfield";
 309              $userparams = array_merge($userparams, $params);
 310  
 311              $order = 'incourse DESC, ' . $order;
 312          }
 313  
 314          // Get result (first 30 rows only) from database. Take a couple spare in case we have to
 315          // drop some.
 316          $result = [];
 317          $got = 0;
 318          $pos = 0;
 319          $readcount = $max + 2;
 320          for ($i = 0; $i < $querylimit; $i++) {
 321              $rawresult = $DB->get_records_sql("
 322                      SELECT $selectfields
 323                        FROM ($userquery) users
 324                        JOIN {user} u ON u.id = users.id
 325                       WHERE $where
 326                    ORDER BY $order", array_merge($userparams, $whereparams, $orderparams),
 327                      $pos, $readcount);
 328              foreach ($rawresult as $user) {
 329                  // Skip guest.
 330                  if ($user->username === 'guest') {
 331                      continue;
 332                  }
 333                  // Check user can really view profile (there are per-user cases where this could
 334                  // be different for some reason, this is the same check used by the profile view pages
 335                  // to double-check that it is OK).
 336                  if (!user_can_view_profile($user)) {
 337                      continue;
 338                  }
 339                  $result[] = $user;
 340                  $got++;
 341                  if ($got >= $max) {
 342                      break;
 343                  }
 344              }
 345  
 346              if ($got >= $max) {
 347                  // All necessary results obtained.
 348                  break;
 349              }
 350              if (count($rawresult) < $readcount) {
 351                  // No more results from database.
 352                  break;
 353              }
 354              $pos += $readcount;
 355          }
 356  
 357          return $result;
 358      }
 359  
 360      /**
 361       * Gets an SQL query that lists all enrolled user ids on any course where the current
 362       * user has the specified capability. Helper function used for searching users.
 363       *
 364       * @param string $capability Required capability
 365       * @return array Array containing SQL and params, or two nulls if there are no courses
 366       */
 367      protected static function get_enrolled_sql_on_courses_with_capability($capability) {
 368          // Get all courses where user have the capability.
 369          $courses = get_user_capability_course($capability, null, true,
 370                  implode(',', array_values(context_helper::get_preload_record_columns('ctx'))));
 371          if (!$courses) {
 372              return [null, null];
 373          }
 374  
 375          // Loop around all courses getting the SQL for enrolled users. Note: This query could
 376          // probably be more efficient (without the union) if get_enrolled_sql had a way to
 377          // pass an array of courseids, but it doesn't.
 378          $unionsql = '';
 379          $unionparams = [];
 380          foreach ($courses as $course) {
 381              // Get SQL to list user ids enrolled in this course.
 382              \context_helper::preload_from_record($course);
 383              list ($sql, $params) = get_enrolled_sql(\context_course::instance($course->id));
 384  
 385              // Combine to a big union query.
 386              if ($unionsql) {
 387                  $unionsql .= ' UNION ';
 388              }
 389              $unionsql .= $sql;
 390              $unionparams = array_merge($unionparams, $params);
 391          }
 392  
 393          return [$unionsql, $unionparams];
 394      }
 395  
 396      /**
 397       * Helper function to return dummy noreply user record.
 398       *
 399       * @return stdClass
 400       */
 401      protected static function get_dummy_user_record() {
 402          global $CFG;
 403  
 404          $dummyuser = new stdClass();
 405          $dummyuser->id = self::NOREPLY_USER;
 406          $dummyuser->email = $CFG->noreplyaddress;
 407          $dummyuser->firstname = get_string('noreplyname');
 408          $dummyuser->username = 'noreply';
 409          $dummyuser->lastname = '';
 410          $dummyuser->confirmed = 1;
 411          $dummyuser->suspended = 0;
 412          $dummyuser->deleted = 0;
 413          $dummyuser->picture = 0;
 414          $dummyuser->auth = 'manual';
 415          $dummyuser->firstnamephonetic = '';
 416          $dummyuser->lastnamephonetic = '';
 417          $dummyuser->middlename = '';
 418          $dummyuser->alternatename = '';
 419          $dummyuser->imagealt = '';
 420          return $dummyuser;
 421      }
 422  
 423      /**
 424       * Return noreply user record, this is currently used in messaging
 425       * system only for sending messages from noreply email.
 426       * It will return record of $CFG->noreplyuserid if set else return dummy
 427       * user object with hard-coded $user->emailstop = 1 so noreply can be sent to user.
 428       *
 429       * @return stdClass user record.
 430       */
 431      public static function get_noreply_user() {
 432          global $CFG;
 433  
 434          if (!empty(self::$noreplyuser)) {
 435              return self::$noreplyuser;
 436          }
 437  
 438          // If noreply user is set then use it, else create one.
 439          if (!empty($CFG->noreplyuserid)) {
 440              self::$noreplyuser = self::get_user($CFG->noreplyuserid);
 441              self::$noreplyuser->emailstop = 1; // Force msg stop for this user.
 442              return self::$noreplyuser;
 443          } else {
 444              // Do not cache the dummy user record to avoid language internationalization issues.
 445              $noreplyuser = self::get_dummy_user_record();
 446              $noreplyuser->maildisplay = '1'; // Show to all.
 447              $noreplyuser->emailstop = 1;
 448              return $noreplyuser;
 449          }
 450      }
 451  
 452      /**
 453       * Return support user record, this is currently used in messaging
 454       * system only for sending messages to support email.
 455       * $CFG->supportuserid is set then returns user record
 456       * $CFG->supportemail is set then return dummy record with $CFG->supportemail
 457       * else return admin user record with hard-coded $user->emailstop = 0, so user
 458       * gets support message.
 459       *
 460       * @return stdClass user record.
 461       */
 462      public static function get_support_user() {
 463          global $CFG;
 464  
 465          if (!empty(self::$supportuser)) {
 466              return self::$supportuser;
 467          }
 468  
 469          // If custom support user is set then use it, else if supportemail is set then use it, else use noreply.
 470          if (!empty($CFG->supportuserid)) {
 471              self::$supportuser = self::get_user($CFG->supportuserid, '*', MUST_EXIST);
 472          } else if (empty(self::$supportuser) && !empty($CFG->supportemail)) {
 473              // Try sending it to support email if support user is not set.
 474              $supportuser = self::get_dummy_user_record();
 475              $supportuser->id = self::SUPPORT_USER;
 476              $supportuser->email = $CFG->supportemail;
 477              if ($CFG->supportname) {
 478                  $supportuser->firstname = $CFG->supportname;
 479              }
 480              $supportuser->username = 'support';
 481              $supportuser->maildisplay = '1'; // Show to all.
 482              // Unset emailstop to make sure support message is sent.
 483              $supportuser->emailstop = 0;
 484              return $supportuser;
 485          }
 486  
 487          // Send support msg to admin user if nothing is set above.
 488          if (empty(self::$supportuser)) {
 489              self::$supportuser = get_admin();
 490          }
 491  
 492          // Unset emailstop to make sure support message is sent.
 493          self::$supportuser->emailstop = 0;
 494          return self::$supportuser;
 495      }
 496  
 497      /**
 498       * Reset self::$noreplyuser and self::$supportuser.
 499       * This is only used by phpunit, and there is no other use case for this function.
 500       * Please don't use it outside phpunit.
 501       */
 502      public static function reset_internal_users() {
 503          if (PHPUNIT_TEST) {
 504              self::$noreplyuser = false;
 505              self::$supportuser = false;
 506          } else {
 507              debugging('reset_internal_users() should not be used outside phpunit.', DEBUG_DEVELOPER);
 508          }
 509      }
 510  
 511      /**
 512       * Return true if user id is greater than 0 and alternatively check db.
 513       *
 514       * @param int $userid user id.
 515       * @param bool $checkdb if true userid will be checked in db. By default it's false, and
 516       *                      userid is compared with 0 for performance.
 517       * @return bool true is real user else false.
 518       */
 519      public static function is_real_user($userid, $checkdb = false) {
 520          global $DB;
 521  
 522          if ($userid <= 0) {
 523              return false;
 524          }
 525          if ($checkdb) {
 526              return $DB->record_exists('user', array('id' => $userid));
 527          } else {
 528              return true;
 529          }
 530      }
 531  
 532      /**
 533       * Determine whether the given user ID is that of the current user. Useful for components implementing permission callbacks
 534       * for preferences consumed by {@see fill_preferences_cache}
 535       *
 536       * @param stdClass $user
 537       * @return bool
 538       */
 539      public static function is_current_user(stdClass $user): bool {
 540          global $USER;
 541          return $user->id == $USER->id;
 542      }
 543  
 544      /**
 545       * Check if the given user is an active user in the site.
 546       *
 547       * @param  stdClass  $user         user object
 548       * @param  boolean $checksuspended whether to check if the user has the account suspended
 549       * @param  boolean $checknologin   whether to check if the user uses the nologin auth method
 550       * @throws moodle_exception
 551       * @since  Moodle 3.0
 552       */
 553      public static function require_active_user($user, $checksuspended = false, $checknologin = false) {
 554  
 555          if (!self::is_real_user($user->id)) {
 556              throw new moodle_exception('invaliduser', 'error');
 557          }
 558  
 559          if ($user->deleted) {
 560              throw new moodle_exception('userdeleted');
 561          }
 562  
 563          if (empty($user->confirmed)) {
 564              throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username);
 565          }
 566  
 567          if (isguestuser($user)) {
 568              throw new moodle_exception('guestsarenotallowed', 'error');
 569          }
 570  
 571          if ($checksuspended and $user->suspended) {
 572              throw new moodle_exception('suspended', 'auth');
 573          }
 574  
 575          if ($checknologin and $user->auth == 'nologin') {
 576              throw new moodle_exception('suspended', 'auth');
 577          }
 578      }
 579  
 580      /**
 581       * Updates the provided users profile picture based upon the expected fields returned from the edit or edit_advanced forms.
 582       *
 583       * @param stdClass $usernew An object that contains some information about the user being updated
 584       * @param array $filemanageroptions
 585       * @return bool True if the user was updated, false if it stayed the same.
 586       */
 587      public static function update_picture(stdClass $usernew, $filemanageroptions = array()) {
 588          global $CFG, $DB;
 589          require_once("$CFG->libdir/gdlib.php");
 590  
 591          $context = context_user::instance($usernew->id, MUST_EXIST);
 592          $user = core_user::get_user($usernew->id, 'id, picture', MUST_EXIST);
 593  
 594          $newpicture = $user->picture;
 595          // Get file_storage to process files.
 596          $fs = get_file_storage();
 597          if (!empty($usernew->deletepicture)) {
 598              // The user has chosen to delete the selected users picture.
 599              $fs->delete_area_files($context->id, 'user', 'icon'); // Drop all images in area.
 600              $newpicture = 0;
 601          }
 602  
 603          // Save newly uploaded file, this will avoid context mismatch for newly created users.
 604          if (!isset($usernew->imagefile)) {
 605              $usernew->imagefile = 0;
 606          }
 607          file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
 608          if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
 609              // Get file which was uploaded in draft area.
 610              foreach ($iconfiles as $file) {
 611                  if (!$file->is_directory()) {
 612                      break;
 613                  }
 614              }
 615              // Copy file to temporary location and the send it for processing icon.
 616              if ($iconfile = $file->copy_content_to_temp()) {
 617                  // There is a new image that has been uploaded.
 618                  // Process the new image and set the user to make use of it.
 619                  // NOTE: Uploaded images always take over Gravatar.
 620                  $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
 621                  // Delete temporary file.
 622                  @unlink($iconfile);
 623                  // Remove uploaded file.
 624                  $fs->delete_area_files($context->id, 'user', 'newicon');
 625              } else {
 626                  // Something went wrong while creating temp file.
 627                  // Remove uploaded file.
 628                  $fs->delete_area_files($context->id, 'user', 'newicon');
 629                  return false;
 630              }
 631          }
 632  
 633          if ($newpicture != $user->picture) {
 634              $DB->set_field('user', 'picture', $newpicture, array('id' => $user->id));
 635              return true;
 636          } else {
 637              return false;
 638          }
 639      }
 640  
 641  
 642  
 643      /**
 644       * Definition of user profile fields and the expected parameter type for data validation.
 645       *
 646       * array(
 647       *     'property_name' => array(       // The user property to be checked. Should match the field on the user table.
 648       *          'null' => NULL_ALLOWED,    // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
 649       *          'type' => PARAM_TYPE,      // Expected parameter type of the user field.
 650       *          'choices' => array(1, 2..) // An array of accepted values of the user field.
 651       *          'default' => $CFG->setting // An default value for the field.
 652       *     )
 653       * )
 654       *
 655       * The fields choices and default are optional.
 656       *
 657       * @return void
 658       */
 659      protected static function fill_properties_cache() {
 660          global $CFG, $SESSION;
 661          if (self::$propertiescache !== null) {
 662              return;
 663          }
 664  
 665          // Array of user fields properties and expected parameters.
 666          // Every new field on the user table should be added here otherwise it won't be validated.
 667          $fields = array();
 668          $fields['id'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 669          $fields['auth'] = array('type' => PARAM_AUTH, 'null' => NULL_NOT_ALLOWED);
 670          $fields['confirmed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 671          $fields['policyagreed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 672          $fields['deleted'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 673          $fields['suspended'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 674          $fields['mnethostid'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 675          $fields['username'] = array('type' => PARAM_USERNAME, 'null' => NULL_NOT_ALLOWED);
 676          $fields['password'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
 677          $fields['idnumber'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
 678          $fields['firstname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 679          $fields['lastname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 680          $fields['surname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 681          $fields['email'] = array('type' => PARAM_RAW_TRIMMED, 'null' => NULL_NOT_ALLOWED);
 682          $fields['emailstop'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0);
 683          $fields['phone1'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 684          $fields['phone2'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 685          $fields['institution'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
 686          $fields['department'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
 687          $fields['address'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
 688          $fields['city'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->defaultcity);
 689          $fields['country'] = array('type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->country,
 690                  'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
 691          $fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED,
 692                  'default' => (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang,
 693                  'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
 694          $fields['calendartype'] = array('type' => PARAM_PLUGIN, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
 695                  'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
 696          $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
 697                  'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
 698          $fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED,
 699                  'default' => core_date::get_server_timezone()); // Must not use choices here: timezones can come and go.
 700          $fields['firstaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 701          $fields['lastaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 702          $fields['lastlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 703          $fields['currentlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 704          $fields['lastip'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 705          $fields['secret'] = array('type' => PARAM_ALPHANUM, 'null' => NULL_NOT_ALLOWED);
 706          $fields['picture'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 707          $fields['description'] = array('type' => PARAM_RAW, 'null' => NULL_ALLOWED);
 708          $fields['descriptionformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 709          $fields['mailformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 710                  'default' => $CFG->defaultpreference_mailformat);
 711          $fields['maildigest'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 712                  'default' => $CFG->defaultpreference_maildigest);
 713          $fields['maildisplay'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 714                  'default' => $CFG->defaultpreference_maildisplay);
 715          $fields['autosubscribe'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 716                  'default' => $CFG->defaultpreference_autosubscribe);
 717          $fields['trackforums'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 718                  'default' => $CFG->defaultpreference_trackforums);
 719          $fields['timecreated'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 720          $fields['timemodified'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 721          $fields['trustbitmask'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 722          $fields['imagealt'] = array('type' => PARAM_TEXT, 'null' => NULL_ALLOWED);
 723          $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 724          $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 725          $fields['middlename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 726          $fields['alternatename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 727  
 728          self::$propertiescache = $fields;
 729      }
 730  
 731      /**
 732       * Get properties of a user field.
 733       *
 734       * @param string $property property name to be retrieved.
 735       * @throws coding_exception if the requested property name is invalid.
 736       * @return array the property definition.
 737       */
 738      public static function get_property_definition($property) {
 739  
 740          self::fill_properties_cache();
 741  
 742          if (!array_key_exists($property, self::$propertiescache)) {
 743              throw new coding_exception('Invalid property requested.');
 744          }
 745  
 746          return self::$propertiescache[$property];
 747      }
 748  
 749      /**
 750       * Validate user data.
 751       *
 752       * This method just validates each user field and return an array of errors. It doesn't clean the data,
 753       * the methods clean() and clean_field() should be used for this purpose.
 754       *
 755       * @param stdClass|array $data user data object or array to be validated.
 756       * @return array|true $errors array of errors found on the user object, true if the validation passed.
 757       */
 758      public static function validate($data) {
 759          // Get all user profile fields definition.
 760          self::fill_properties_cache();
 761  
 762          foreach ($data as $property => $value) {
 763              try {
 764                  if (isset(self::$propertiescache[$property])) {
 765                      validate_param($value, self::$propertiescache[$property]['type'], self::$propertiescache[$property]['null']);
 766                  }
 767                  // Check that the value is part of a list of allowed values.
 768                  if (!empty(self::$propertiescache[$property]['choices']) &&
 769                          !isset(self::$propertiescache[$property]['choices'][$value])) {
 770                      throw new invalid_parameter_exception($value);
 771                  }
 772              } catch (invalid_parameter_exception $e) {
 773                  $errors[$property] = $e->getMessage();
 774              }
 775          }
 776  
 777          return empty($errors) ? true : $errors;
 778      }
 779  
 780      /**
 781       * Clean the properties cache.
 782       *
 783       * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
 784       * Intended for use only for testing, phpunit calls this before every test.
 785       */
 786      public static function reset_caches() {
 787          self::$propertiescache = null;
 788      }
 789  
 790      /**
 791       * Clean the user data.
 792       *
 793       * @param stdClass|array $user the user data to be validated against properties definition.
 794       * @return stdClass $user the cleaned user data.
 795       */
 796      public static function clean_data($user) {
 797          if (empty($user)) {
 798              return $user;
 799          }
 800  
 801          foreach ($user as $field => $value) {
 802              // Get the property parameter type and do the cleaning.
 803              try {
 804                  $user->$field = core_user::clean_field($value, $field);
 805              } catch (coding_exception $e) {
 806                  debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
 807              }
 808          }
 809  
 810          return $user;
 811      }
 812  
 813      /**
 814       * Clean a specific user field.
 815       *
 816       * @param string $data the user field data to be cleaned.
 817       * @param string $field the user field name on the property definition cache.
 818       * @return string the cleaned user data.
 819       */
 820      public static function clean_field($data, $field) {
 821          if (empty($data) || empty($field)) {
 822              return $data;
 823          }
 824  
 825          try {
 826              $type = core_user::get_property_type($field);
 827  
 828              if (isset(self::$propertiescache[$field]['choices'])) {
 829                  if (!array_key_exists($data, self::$propertiescache[$field]['choices'])) {
 830                      if (isset(self::$propertiescache[$field]['default'])) {
 831                          $data = self::$propertiescache[$field]['default'];
 832                      } else {
 833                          $data = '';
 834                      }
 835                  } else {
 836                      return $data;
 837                  }
 838              } else {
 839                  $data = clean_param($data, $type);
 840              }
 841          } catch (coding_exception $e) {
 842              debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
 843          }
 844  
 845          return $data;
 846      }
 847  
 848      /**
 849       * Get the parameter type of the property.
 850       *
 851       * @param string $property property name to be retrieved.
 852       * @throws coding_exception if the requested property name is invalid.
 853       * @return int the property parameter type.
 854       */
 855      public static function get_property_type($property) {
 856  
 857          self::fill_properties_cache();
 858  
 859          if (!array_key_exists($property, self::$propertiescache)) {
 860              throw new coding_exception('Invalid property requested: ' . $property);
 861          }
 862  
 863          return self::$propertiescache[$property]['type'];
 864      }
 865  
 866      /**
 867       * Discover if the property is NULL_ALLOWED or NULL_NOT_ALLOWED.
 868       *
 869       * @param string $property property name to be retrieved.
 870       * @throws coding_exception if the requested property name is invalid.
 871       * @return bool true if the property is NULL_ALLOWED, false otherwise.
 872       */
 873      public static function get_property_null($property) {
 874  
 875          self::fill_properties_cache();
 876  
 877          if (!array_key_exists($property, self::$propertiescache)) {
 878              throw new coding_exception('Invalid property requested: ' . $property);
 879          }
 880  
 881          return self::$propertiescache[$property]['null'];
 882      }
 883  
 884      /**
 885       * Get the choices of the property.
 886       *
 887       * This is a helper method to validate a value against a list of acceptable choices.
 888       * For instance: country, language, themes and etc.
 889       *
 890       * @param string $property property name to be retrieved.
 891       * @throws coding_exception if the requested property name is invalid or if it does not has a list of choices.
 892       * @return array the property parameter type.
 893       */
 894      public static function get_property_choices($property) {
 895  
 896          self::fill_properties_cache();
 897  
 898          if (!array_key_exists($property, self::$propertiescache) && !array_key_exists('choices',
 899                  self::$propertiescache[$property])) {
 900  
 901              throw new coding_exception('Invalid property requested, or the property does not has a list of choices.');
 902          }
 903  
 904          return self::$propertiescache[$property]['choices'];
 905      }
 906  
 907      /**
 908       * Get the property default.
 909       *
 910       * This method gets the default value of a field (if exists).
 911       *
 912       * @param string $property property name to be retrieved.
 913       * @throws coding_exception if the requested property name is invalid or if it does not has a default value.
 914       * @return string the property default value.
 915       */
 916      public static function get_property_default($property) {
 917  
 918          self::fill_properties_cache();
 919  
 920          if (!array_key_exists($property, self::$propertiescache) || !isset(self::$propertiescache[$property]['default'])) {
 921              throw new coding_exception('Invalid property requested, or the property does not has a default value.');
 922          }
 923  
 924          return self::$propertiescache[$property]['default'];
 925      }
 926  
 927      /**
 928       * Definition of updateable user preferences and rules for data and access validation.
 929       *
 930       * array(
 931       *     'preferencename' => array(      // Either exact preference name or a regular expression.
 932       *          'null' => NULL_ALLOWED,    // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
 933       *          'type' => PARAM_TYPE,      // Expected parameter type of the user field - mandatory
 934       *          'choices' => array(1, 2..) // An array of accepted values of the user field - optional
 935       *          'default' => $CFG->setting // An default value for the field - optional
 936       *          'isregex' => false/true    // Whether the name of the preference is a regular expression (default false).
 937       *          'permissioncallback' => callable // Function accepting arguments ($user, $preferencename) that checks if current user
 938       *                                     // is allowed to modify this preference for given user.
 939       *                                     // If not specified core_user::default_preference_permission_check() will be assumed.
 940       *          'cleancallback' => callable // Custom callback for cleaning value if something more difficult than just type/choices is needed
 941       *                                     // accepts arguments ($value, $preferencename)
 942       *     )
 943       * )
 944       *
 945       * @return void
 946       */
 947      protected static function fill_preferences_cache() {
 948          global $CFG;
 949  
 950          if (self::$preferencescache !== null) {
 951              return;
 952          }
 953  
 954          // Array of user preferences and expected types/values.
 955          // Every preference that can be updated directly by user should be added here.
 956          $preferences = array();
 957          $preferences['auth_forcepasswordchange'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'choices' => array(0, 1),
 958              'permissioncallback' => function($user, $preferencename) {
 959                  global $USER;
 960                  $systemcontext = context_system::instance();
 961                  return ($USER->id != $user->id && (has_capability('moodle/user:update', $systemcontext) ||
 962                          ($user->timecreated > time() - 10 && has_capability('moodle/user:create', $systemcontext))));
 963              });
 964          $preferences['forum_markasreadonnotification'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
 965              'choices' => array(0, 1));
 966          $preferences['htmleditor'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED,
 967              'cleancallback' => function($value, $preferencename) {
 968                  if (empty($value) || !array_key_exists($value, core_component::get_plugin_list('editor'))) {
 969                      return null;
 970                  }
 971                  return $value;
 972              });
 973          $preferences['badgeprivacysetting'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
 974              'choices' => array(0, 1), 'permissioncallback' => function($user, $preferencename) {
 975                  global $CFG;
 976                  return !empty($CFG->enablebadges) && self::is_current_user($user);
 977              });
 978          $preferences['blogpagesize'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 10,
 979              'permissioncallback' => function($user, $preferencename) {
 980                  return self::is_current_user($user) && has_capability('moodle/blog:view', context_system::instance());
 981              });
 982          $preferences['filemanager_recentviewmode'] = [
 983              'type' => PARAM_INT,
 984              'null' => NULL_NOT_ALLOWED,
 985              'default' => 1,
 986              'choices' => [1, 2, 3],
 987              'permissioncallback' => [static::class, 'is_current_user'],
 988          ];
 989          $preferences['filepicker_recentrepository'] = [
 990              'type' => PARAM_INT,
 991              'null' => NULL_NOT_ALLOWED,
 992              'permissioncallback' => [static::class, 'is_current_user'],
 993          ];
 994          $preferences['filepicker_recentlicense'] = [
 995              'type' => PARAM_SAFEDIR,
 996              'null' => NULL_NOT_ALLOWED,
 997              'permissioncallback' => [static::class, 'is_current_user'],
 998          ];
 999          $preferences['filepicker_recentviewmode'] = [
1000              'type' => PARAM_INT,
1001              'null' => NULL_NOT_ALLOWED,
1002              'default' => 1,
1003              'choices' => [1, 2, 3],
1004              'permissioncallback' => [static::class, 'is_current_user'],
1005          ];
1006          $preferences['userselector_optionscollapsed'] = [
1007              'type' => PARAM_BOOL,
1008              'null' => NULL_NOT_ALLOWED,
1009              'default' => true,
1010              'permissioncallback' => [static::class, 'is_current_user'],
1011          ];
1012          $preferences['userselector_autoselectunique'] = [
1013              'type' => PARAM_BOOL,
1014              'null' => NULL_NOT_ALLOWED,
1015              'default' => false,
1016              'permissioncallback' => [static::class, 'is_current_user'],
1017          ];
1018          $preferences['userselector_preserveselected'] = [
1019              'type' => PARAM_BOOL,
1020              'null' => NULL_NOT_ALLOWED,
1021              'default' => false,
1022              'permissioncallback' => [static::class, 'is_current_user'],
1023          ];
1024          $preferences['userselector_searchtype'] = [
1025              'type' => PARAM_INT,
1026              'null' => NULL_NOT_ALLOWED,
1027              'default' => USER_SEARCH_STARTS_WITH,
1028              'permissioncallback' => [static::class, 'is_current_user'],
1029          ];
1030          $preferences['question_bank_advanced_search'] = [
1031              'type' => PARAM_BOOL,
1032              'null' => NULL_NOT_ALLOWED,
1033              'default' => false,
1034              'permissioncallback' => [static::class, 'is_current_user'],
1035          ];
1036  
1037          $choices = [HOMEPAGE_SITE];
1038          if (!empty($CFG->enabledashboard)) {
1039              $choices[] = HOMEPAGE_MY;
1040          }
1041          $choices[] = HOMEPAGE_MYCOURSES;
1042          $preferences['user_home_page_preference'] = [
1043              'type' => PARAM_INT,
1044              'null' => NULL_ALLOWED,
1045              'default' => get_default_home_page(),
1046              'choices' => $choices,
1047              'permissioncallback' => function ($user, $preferencename) {
1048                  global $CFG;
1049                  return self::is_current_user($user) &&
1050                      (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
1051              }
1052          ];
1053  
1054          // Core components that may want to define their preferences.
1055          // List of core components implementing callback is hardcoded here for performance reasons.
1056          // TODO MDL-58184 cache list of core components implementing a function.
1057          $corecomponents = ['core_message', 'core_calendar', 'core_contentbank'];
1058          foreach ($corecomponents as $component) {
1059              if (($pluginpreferences = component_callback($component, 'user_preferences')) && is_array($pluginpreferences)) {
1060                  $preferences += $pluginpreferences;
1061              }
1062          }
1063  
1064          // Plugins that may define their preferences.
1065          if ($pluginsfunction = get_plugins_with_function('user_preferences')) {
1066              foreach ($pluginsfunction as $plugintype => $plugins) {
1067                  foreach ($plugins as $function) {
1068                      if (($pluginpreferences = call_user_func($function)) && is_array($pluginpreferences)) {
1069                          $preferences += $pluginpreferences;
1070                      }
1071                  }
1072              }
1073          }
1074  
1075          self::$preferencescache = $preferences;
1076      }
1077  
1078      /**
1079       * Retrieves the preference definition
1080       *
1081       * @param string $preferencename
1082       * @return array
1083       */
1084      protected static function get_preference_definition($preferencename) {
1085          self::fill_preferences_cache();
1086  
1087          foreach (self::$preferencescache as $key => $preference) {
1088              if (empty($preference['isregex'])) {
1089                  if ($key === $preferencename) {
1090                      return $preference;
1091                  }
1092              } else {
1093                  if (preg_match($key, $preferencename)) {
1094                      return $preference;
1095                  }
1096              }
1097          }
1098  
1099          throw new coding_exception('Invalid preference requested.');
1100      }
1101  
1102      /**
1103       * Default callback used for checking if current user is allowed to change permission of user $user
1104       *
1105       * @param stdClass $user
1106       * @param string $preferencename
1107       * @return bool
1108       */
1109      protected static function default_preference_permission_check($user, $preferencename) {
1110          global $USER;
1111          if (is_mnet_remote_user($user)) {
1112              // Can't edit MNET user.
1113              return false;
1114          }
1115  
1116          if (self::is_current_user($user)) {
1117              // Editing own profile.
1118              $systemcontext = context_system::instance();
1119              return has_capability('moodle/user:editownprofile', $systemcontext);
1120          } else  {
1121              // Teachers, parents, etc.
1122              $personalcontext = context_user::instance($user->id);
1123              if (!has_capability('moodle/user:editprofile', $personalcontext)) {
1124                  return false;
1125              }
1126              if (is_siteadmin($user->id) and !is_siteadmin($USER)) {
1127                  // Only admins may edit other admins.
1128                  return false;
1129              }
1130              return true;
1131          }
1132      }
1133  
1134      /**
1135       * Can current user edit preference of this/another user
1136       *
1137       * @param string $preferencename
1138       * @param stdClass $user
1139       * @return bool
1140       */
1141      public static function can_edit_preference($preferencename, $user) {
1142          if (!isloggedin() || isguestuser()) {
1143              // Guests can not edit anything.
1144              return false;
1145          }
1146  
1147          try {
1148              $definition = self::get_preference_definition($preferencename);
1149          } catch (coding_exception $e) {
1150              return false;
1151          }
1152  
1153          if ($user->deleted || !context_user::instance($user->id, IGNORE_MISSING)) {
1154              // User is deleted.
1155              return false;
1156          }
1157  
1158          if (isset($definition['permissioncallback'])) {
1159              $callback = $definition['permissioncallback'];
1160              if (is_callable($callback)) {
1161                  return call_user_func_array($callback, [$user, $preferencename]);
1162              } else {
1163                  throw new coding_exception('Permission callback for preference ' . s($preferencename) . ' is not callable');
1164                  return false;
1165              }
1166          } else {
1167              return self::default_preference_permission_check($user, $preferencename);
1168          }
1169      }
1170  
1171      /**
1172       * Clean value of a user preference
1173       *
1174       * @param string $value the user preference value to be cleaned.
1175       * @param string $preferencename the user preference name
1176       * @return string the cleaned preference value
1177       */
1178      public static function clean_preference($value, $preferencename) {
1179  
1180          $definition = self::get_preference_definition($preferencename);
1181  
1182          if (isset($definition['type']) && $value !== null) {
1183              $value = clean_param($value, $definition['type']);
1184          }
1185  
1186          if (isset($definition['cleancallback'])) {
1187              $callback = $definition['cleancallback'];
1188              if (is_callable($callback)) {
1189                  return $callback($value, $preferencename);
1190              } else {
1191                  throw new coding_exception('Clean callback for preference ' . s($preferencename) . ' is not callable');
1192              }
1193          } else if ($value === null && (!isset($definition['null']) || $definition['null'] == NULL_ALLOWED)) {
1194              return null;
1195          } else if (isset($definition['choices'])) {
1196              if (!in_array($value, $definition['choices'])) {
1197                  if (isset($definition['default'])) {
1198                      return $definition['default'];
1199                  } else {
1200                      $first = reset($definition['choices']);
1201                      return $first;
1202                  }
1203              } else {
1204                  return $value;
1205              }
1206          } else {
1207              if ($value === null) {
1208                  return isset($definition['default']) ? $definition['default'] : '';
1209              }
1210              return $value;
1211          }
1212      }
1213  
1214      /**
1215       * Is the user expected to perform an action to start using Moodle properly?
1216       *
1217       * This covers cases such as filling the profile, changing password or agreeing to the site policy.
1218       *
1219       * @param stdClass $user User object, defaults to the current user.
1220       * @return bool
1221       */
1222      public static function awaiting_action(stdClass $user = null): bool {
1223          global $USER;
1224  
1225          if ($user === null) {
1226              $user = $USER;
1227          }
1228  
1229          if (user_not_fully_set_up($user)) {
1230              // Awaiting the user to fill all fields in the profile.
1231              return true;
1232          }
1233  
1234          if (get_user_preferences('auth_forcepasswordchange', false, $user)) {
1235              // Awaiting the user to change their password.
1236              return true;
1237          }
1238  
1239          if (empty($user->policyagreed) && !is_siteadmin($user)) {
1240              $manager = new \core_privacy\local\sitepolicy\manager();
1241  
1242              if ($manager->is_defined(isguestuser($user))) {
1243                  return true;
1244              }
1245          }
1246  
1247          return false;
1248      }
1249  
1250      /**
1251       * Get welcome message.
1252       *
1253       * @return lang_string welcome message
1254       */
1255      public static function welcome_message(): ?lang_string {
1256          global $USER;
1257  
1258          $isloggedinas = \core\session\manager::is_loggedinas();
1259          if (!isloggedin() || isguestuser() || $isloggedinas) {
1260              return null;
1261          }
1262          if (empty($USER->core_welcome_message)) {
1263              $USER->core_welcome_message = true;
1264              $messagekey = 'welcomeback';
1265              if (empty(get_user_preferences('core_user_welcome', null))) {
1266                  $messagekey = 'welcometosite';
1267                  set_user_preference('core_user_welcome', time());
1268              }
1269  
1270              $namefields = [
1271                  'fullname' => fullname($USER),
1272                  'alternativefullname' => fullname($USER, true),
1273              ];
1274  
1275              foreach (\core_user\fields::get_name_fields() as $namefield) {
1276                  $namefields[$namefield] = $USER->{$namefield};
1277              }
1278  
1279              return new lang_string($messagekey, 'core', $namefields);
1280          };
1281          return null;
1282      }
1283  
1284      /**
1285       * Return full name depending on context.
1286       * This function should be used for displaying purposes only as the details may not be the same as it is on database.
1287       *
1288       * @param stdClass $user the person to get details of.
1289       * @param context|null $context The context will be used to determine the visibility of the user's full name.
1290       * @param array $options can include: override - if true, will not use forced firstname/lastname settings
1291       * @return string Full name of the user
1292       */
1293      public static function get_fullname(stdClass $user, context $context = null, array $options = []): string {
1294          global $CFG, $SESSION;
1295  
1296          // Clone the user so that it does not mess up the original object.
1297          $user = clone($user);
1298  
1299          // Override options.
1300          $override = $options["override"] ?? false;
1301  
1302          if (!isset($user->firstname) && !isset($user->lastname)) {
1303              return '';
1304          }
1305  
1306          // Get all of the name fields.
1307          $allnames = \core_user\fields::get_name_fields();
1308          if ($CFG->debugdeveloper) {
1309              $missingfields = [];
1310              foreach ($allnames as $allname) {
1311                  if (!property_exists($user, $allname)) {
1312                      $missingfields[] = $allname;
1313                  }
1314              }
1315              if (!empty($missingfields)) {
1316                  debugging('The following name fields are missing from the user object: ' . implode(', ', $missingfields));
1317              }
1318          }
1319  
1320          if (!$override) {
1321              if (!empty($CFG->forcefirstname)) {
1322                  $user->firstname = $CFG->forcefirstname;
1323              }
1324              if (!empty($CFG->forcelastname)) {
1325                  $user->lastname = $CFG->forcelastname;
1326              }
1327          }
1328  
1329          if (!empty($SESSION->fullnamedisplay)) {
1330              $CFG->fullnamedisplay = $SESSION->fullnamedisplay;
1331          }
1332  
1333          $template = null;
1334          // If the fullnamedisplay setting is available, set the template to that.
1335          if (isset($CFG->fullnamedisplay)) {
1336              $template = $CFG->fullnamedisplay;
1337          }
1338          // If the template is empty, or set to language, return the language string.
1339          if ((empty($template) || $template == 'language') && !$override) {
1340              return get_string('fullnamedisplay', null, $user);
1341          }
1342  
1343          // Check to see if we are displaying according to the alternative full name format.
1344          if ($override) {
1345              if (empty($CFG->alternativefullnameformat) || $CFG->alternativefullnameformat == 'language') {
1346                  // Default to show just the user names according to the fullnamedisplay string.
1347                  return get_string('fullnamedisplay', null, $user);
1348              } else {
1349                  // If the override is true, then change the template to use the complete name.
1350                  $template = $CFG->alternativefullnameformat;
1351              }
1352          }
1353  
1354          $requirednames = array();
1355          // With each name, see if it is in the display name template, and add it to the required names array if it is.
1356          foreach ($allnames as $allname) {
1357              if (strpos($template, $allname) !== false) {
1358                  $requirednames[] = $allname;
1359              }
1360          }
1361  
1362          $displayname = $template;
1363          // Switch in the actual data into the template.
1364          foreach ($requirednames as $altname) {
1365              if (isset($user->$altname)) {
1366                  // Using empty() on the below if statement causes breakages.
1367                  if ((string)$user->$altname == '') {
1368                      $displayname = str_replace($altname, 'EMPTY', $displayname);
1369                  } else {
1370                      $displayname = str_replace($altname, $user->$altname, $displayname);
1371                  }
1372              } else {
1373                  $displayname = str_replace($altname, 'EMPTY', $displayname);
1374              }
1375          }
1376          // Tidy up any misc. characters (Not perfect, but gets most characters).
1377          // Don't remove the "u" at the end of the first expression unless you want garbled characters when combining hiragana or
1378          // katakana and parenthesis.
1379          $patterns = array();
1380          // This regular expression replacement is to fix problems such as 'James () Kirk' Where 'Tiberius' (middlename) has not been
1381          // filled in by a user.
1382          // The special characters are Japanese brackets that are common enough to make allowances for them (not covered by :punct:).
1383          $patterns[] = '/[[:punct:]「」]*EMPTY[[:punct:]「」]*/u';
1384          // This regular expression is to remove any double spaces in the display name.
1385          $patterns[] = '/\s{2,}/u';
1386          foreach ($patterns as $pattern) {
1387              $displayname = preg_replace($pattern, ' ', $displayname);
1388          }
1389  
1390          // Trimming $displayname will help the next check to ensure that we don't have a display name with spaces.
1391          $displayname = trim($displayname);
1392          if (empty($displayname)) {
1393              // Going with just the first name if no alternate fields are filled out. May be changed later depending on what
1394              // people in general feel is a good setting to fall back on.
1395              $displayname = $user->firstname;
1396          }
1397          return $displayname;
1398      }
1399  
1400      /**
1401       * Return profile url depending on context.
1402       *
1403       * @param stdClass $user the person to get details of.
1404       * @param context|null $context The context will be used to determine the visibility of the user's profile url.
1405       * @return moodle_url Profile url of the user
1406       */
1407      public static function get_profile_url(stdClass $user, context $context = null): moodle_url {
1408          if (empty($user->id)) {
1409              throw new coding_exception('User id is required when displaying profile url.');
1410          }
1411  
1412          // Params to be passed to the user view page.
1413          $params = ['id' => $user->id];
1414  
1415          // Get courseid if provided.
1416          if (isset($options['courseid'])) {
1417              $params['courseid'] = $options['courseid'];
1418          }
1419  
1420          // Get courseid from context if provided.
1421          if ($context) {
1422              $coursecontext = $context->get_course_context(false);
1423              if ($coursecontext) {
1424                  $params['courseid'] = $coursecontext->instanceid;
1425              }
1426          }
1427  
1428          // If courseid is not set or is set to site id, then return profile page, otherwise return view page.
1429          if (!isset($params['courseid']) || $params['courseid'] == SITEID) {
1430              return new moodle_url('/user/profile.php', $params);
1431          } else {
1432              return new moodle_url('/user/view.php', $params);
1433          }
1434      }
1435  
1436      /**
1437       * Return user picture depending on context.
1438       * This function should be used for displaying purposes only as the details may not be the same as it is on database.
1439       *
1440       * @param stdClass $user the person to get details of.
1441       * @param context|null $context The context will be used to determine the visibility of the user's picture.
1442       * @param array $options public properties of {@see user_picture} to be overridden
1443       *     - courseid = $this->page->course->id (course id of user profile in link)
1444       *     - size = 35 (size of image)
1445       *     - link = true (make image clickable - the link leads to user profile)
1446       *     - popup = false (open in popup)
1447       *     - alttext = true (add image alt attribute)
1448       *     - class = image class attribute (default 'userpicture')
1449       *     - visibletoscreenreaders = true (whether to be visible to screen readers)
1450       *     - includefullname = false (whether to include the user's full name together with the user picture)
1451       *     - includetoken = false (whether to use a token for authentication. True for current user, int value for other user id)
1452       * @return user_picture User picture object
1453       */
1454      public static function get_profile_picture(stdClass $user, context $context = null, array $options = []): user_picture {
1455          // Create a new user picture object.
1456          $userpicture = new user_picture($user);
1457  
1458          // Override the user picture object with the options provided.
1459          foreach ($options as $key => $value) {
1460              if (property_exists($userpicture, $key)) {
1461                  $userpicture->$key = $value;
1462              }
1463          }
1464  
1465          // Return the user picture.
1466          return $userpicture;
1467      }
1468  
1469      /**
1470       * Get initials for users
1471       *
1472       * @param stdClass $user
1473       * @return string
1474       */
1475      public static function get_initials(stdClass $user): string {
1476          // Get the available name fields.
1477          $namefields = \core_user\fields::get_name_fields();
1478          // Build a dummy user to determine the name format.
1479          $dummyuser = array_combine($namefields, $namefields);
1480          // Determine the name format by using fullname() and passing the dummy user.
1481          $nameformat = fullname((object) $dummyuser);
1482          // Fetch all the available username fields.
1483          $availablefields = order_in_string($namefields, $nameformat);
1484          // We only want the first and last name fields.
1485          if (!empty($availablefields) && count($availablefields) >= 2) {
1486              $availablefields = [reset($availablefields), end($availablefields)];
1487          }
1488          $initials = '';
1489          foreach ($availablefields as $userfieldname) {
1490              if (!empty($user->$userfieldname)) {
1491                  $initials .= mb_substr($user->$userfieldname, 0, 1);
1492              }
1493          }
1494          return $initials;
1495      }
1496  
1497  }