Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   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', false);
 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       * Check if the given user is an active user in the site.
 534       *
 535       * @param  stdClass  $user         user object
 536       * @param  boolean $checksuspended whether to check if the user has the account suspended
 537       * @param  boolean $checknologin   whether to check if the user uses the nologin auth method
 538       * @throws moodle_exception
 539       * @since  Moodle 3.0
 540       */
 541      public static function require_active_user($user, $checksuspended = false, $checknologin = false) {
 542  
 543          if (!self::is_real_user($user->id)) {
 544              throw new moodle_exception('invaliduser', 'error');
 545          }
 546  
 547          if ($user->deleted) {
 548              throw new moodle_exception('userdeleted');
 549          }
 550  
 551          if (empty($user->confirmed)) {
 552              throw new moodle_exception('usernotconfirmed', 'moodle', '', $user->username);
 553          }
 554  
 555          if (isguestuser($user)) {
 556              throw new moodle_exception('guestsarenotallowed', 'error');
 557          }
 558  
 559          if ($checksuspended and $user->suspended) {
 560              throw new moodle_exception('suspended', 'auth');
 561          }
 562  
 563          if ($checknologin and $user->auth == 'nologin') {
 564              throw new moodle_exception('suspended', 'auth');
 565          }
 566      }
 567  
 568      /**
 569       * Updates the provided users profile picture based upon the expected fields returned from the edit or edit_advanced forms.
 570       *
 571       * @param stdClass $usernew An object that contains some information about the user being updated
 572       * @param array $filemanageroptions
 573       * @return bool True if the user was updated, false if it stayed the same.
 574       */
 575      public static function update_picture(stdClass $usernew, $filemanageroptions = array()) {
 576          global $CFG, $DB;
 577          require_once("$CFG->libdir/gdlib.php");
 578  
 579          $context = context_user::instance($usernew->id, MUST_EXIST);
 580          $user = core_user::get_user($usernew->id, 'id, picture', MUST_EXIST);
 581  
 582          $newpicture = $user->picture;
 583          // Get file_storage to process files.
 584          $fs = get_file_storage();
 585          if (!empty($usernew->deletepicture)) {
 586              // The user has chosen to delete the selected users picture.
 587              $fs->delete_area_files($context->id, 'user', 'icon'); // Drop all images in area.
 588              $newpicture = 0;
 589          }
 590  
 591          // Save newly uploaded file, this will avoid context mismatch for newly created users.
 592          if (!isset($usernew->imagefile)) {
 593              $usernew->imagefile = 0;
 594          }
 595          file_save_draft_area_files($usernew->imagefile, $context->id, 'user', 'newicon', 0, $filemanageroptions);
 596          if (($iconfiles = $fs->get_area_files($context->id, 'user', 'newicon')) && count($iconfiles) == 2) {
 597              // Get file which was uploaded in draft area.
 598              foreach ($iconfiles as $file) {
 599                  if (!$file->is_directory()) {
 600                      break;
 601                  }
 602              }
 603              // Copy file to temporary location and the send it for processing icon.
 604              if ($iconfile = $file->copy_content_to_temp()) {
 605                  // There is a new image that has been uploaded.
 606                  // Process the new image and set the user to make use of it.
 607                  // NOTE: Uploaded images always take over Gravatar.
 608                  $newpicture = (int)process_new_icon($context, 'user', 'icon', 0, $iconfile);
 609                  // Delete temporary file.
 610                  @unlink($iconfile);
 611                  // Remove uploaded file.
 612                  $fs->delete_area_files($context->id, 'user', 'newicon');
 613              } else {
 614                  // Something went wrong while creating temp file.
 615                  // Remove uploaded file.
 616                  $fs->delete_area_files($context->id, 'user', 'newicon');
 617                  return false;
 618              }
 619          }
 620  
 621          if ($newpicture != $user->picture) {
 622              $DB->set_field('user', 'picture', $newpicture, array('id' => $user->id));
 623              return true;
 624          } else {
 625              return false;
 626          }
 627      }
 628  
 629  
 630  
 631      /**
 632       * Definition of user profile fields and the expected parameter type for data validation.
 633       *
 634       * array(
 635       *     'property_name' => array(       // The user property to be checked. Should match the field on the user table.
 636       *          'null' => NULL_ALLOWED,    // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
 637       *          'type' => PARAM_TYPE,      // Expected parameter type of the user field.
 638       *          'choices' => array(1, 2..) // An array of accepted values of the user field.
 639       *          'default' => $CFG->setting // An default value for the field.
 640       *     )
 641       * )
 642       *
 643       * The fields choices and default are optional.
 644       *
 645       * @return void
 646       */
 647      protected static function fill_properties_cache() {
 648          global $CFG, $SESSION;
 649          if (self::$propertiescache !== null) {
 650              return;
 651          }
 652  
 653          // Array of user fields properties and expected parameters.
 654          // Every new field on the user table should be added here otherwise it won't be validated.
 655          $fields = array();
 656          $fields['id'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 657          $fields['auth'] = array('type' => PARAM_AUTH, 'null' => NULL_NOT_ALLOWED);
 658          $fields['confirmed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 659          $fields['policyagreed'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 660          $fields['deleted'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 661          $fields['suspended'] = array('type' => PARAM_BOOL, 'null' => NULL_NOT_ALLOWED);
 662          $fields['mnethostid'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 663          $fields['username'] = array('type' => PARAM_USERNAME, 'null' => NULL_NOT_ALLOWED);
 664          $fields['password'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
 665          $fields['idnumber'] = array('type' => PARAM_RAW, 'null' => NULL_NOT_ALLOWED);
 666          $fields['firstname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 667          $fields['lastname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 668          $fields['surname'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 669          $fields['email'] = array('type' => PARAM_RAW_TRIMMED, 'null' => NULL_NOT_ALLOWED);
 670          $fields['emailstop'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 0);
 671          $fields['phone1'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 672          $fields['phone2'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 673          $fields['institution'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
 674          $fields['department'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
 675          $fields['address'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED);
 676          $fields['city'] = array('type' => PARAM_TEXT, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->defaultcity);
 677          $fields['country'] = array('type' => PARAM_ALPHA, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->country,
 678                  'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_countries(true, true)));
 679          $fields['lang'] = array('type' => PARAM_LANG, 'null' => NULL_NOT_ALLOWED,
 680                  'default' => (!empty($CFG->autolangusercreation) && !empty($SESSION->lang)) ? $SESSION->lang : $CFG->lang,
 681                  'choices' => array_merge(array('' => ''), get_string_manager()->get_list_of_translations(false)));
 682          $fields['calendartype'] = array('type' => PARAM_PLUGIN, 'null' => NULL_NOT_ALLOWED, 'default' => $CFG->calendartype,
 683                  'choices' => array_merge(array('' => ''), \core_calendar\type_factory::get_list_of_calendar_types()));
 684          $fields['theme'] = array('type' => PARAM_THEME, 'null' => NULL_NOT_ALLOWED,
 685                  'default' => theme_config::DEFAULT_THEME, 'choices' => array_merge(array('' => ''), get_list_of_themes()));
 686          $fields['timezone'] = array('type' => PARAM_TIMEZONE, 'null' => NULL_NOT_ALLOWED,
 687                  'default' => core_date::get_server_timezone()); // Must not use choices here: timezones can come and go.
 688          $fields['firstaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 689          $fields['lastaccess'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 690          $fields['lastlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 691          $fields['currentlogin'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 692          $fields['lastip'] = array('type' => PARAM_NOTAGS, 'null' => NULL_NOT_ALLOWED);
 693          $fields['secret'] = array('type' => PARAM_ALPHANUM, 'null' => NULL_NOT_ALLOWED);
 694          $fields['picture'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 695          $fields['description'] = array('type' => PARAM_RAW, 'null' => NULL_ALLOWED);
 696          $fields['descriptionformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 697          $fields['mailformat'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 698                  'default' => $CFG->defaultpreference_mailformat);
 699          $fields['maildigest'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 700                  'default' => $CFG->defaultpreference_maildigest);
 701          $fields['maildisplay'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 702                  'default' => $CFG->defaultpreference_maildisplay);
 703          $fields['autosubscribe'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 704                  'default' => $CFG->defaultpreference_autosubscribe);
 705          $fields['trackforums'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED,
 706                  'default' => $CFG->defaultpreference_trackforums);
 707          $fields['timecreated'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 708          $fields['timemodified'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 709          $fields['trustbitmask'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED);
 710          $fields['imagealt'] = array('type' => PARAM_TEXT, 'null' => NULL_ALLOWED);
 711          $fields['lastnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 712          $fields['firstnamephonetic'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 713          $fields['middlename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 714          $fields['alternatename'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED);
 715  
 716          self::$propertiescache = $fields;
 717      }
 718  
 719      /**
 720       * Get properties of a user field.
 721       *
 722       * @param string $property property name to be retrieved.
 723       * @throws coding_exception if the requested property name is invalid.
 724       * @return array the property definition.
 725       */
 726      public static function get_property_definition($property) {
 727  
 728          self::fill_properties_cache();
 729  
 730          if (!array_key_exists($property, self::$propertiescache)) {
 731              throw new coding_exception('Invalid property requested.');
 732          }
 733  
 734          return self::$propertiescache[$property];
 735      }
 736  
 737      /**
 738       * Validate user data.
 739       *
 740       * This method just validates each user field and return an array of errors. It doesn't clean the data,
 741       * the methods clean() and clean_field() should be used for this purpose.
 742       *
 743       * @param stdClass|array $data user data object or array to be validated.
 744       * @return array|true $errors array of errors found on the user object, true if the validation passed.
 745       */
 746      public static function validate($data) {
 747          // Get all user profile fields definition.
 748          self::fill_properties_cache();
 749  
 750          foreach ($data as $property => $value) {
 751              try {
 752                  if (isset(self::$propertiescache[$property])) {
 753                      validate_param($value, self::$propertiescache[$property]['type'], self::$propertiescache[$property]['null']);
 754                  }
 755                  // Check that the value is part of a list of allowed values.
 756                  if (!empty(self::$propertiescache[$property]['choices']) &&
 757                          !isset(self::$propertiescache[$property]['choices'][$value])) {
 758                      throw new invalid_parameter_exception($value);
 759                  }
 760              } catch (invalid_parameter_exception $e) {
 761                  $errors[$property] = $e->getMessage();
 762              }
 763          }
 764  
 765          return empty($errors) ? true : $errors;
 766      }
 767  
 768      /**
 769       * Clean the properties cache.
 770       *
 771       * During unit tests we need to be able to reset all caches so that each new test starts in a known state.
 772       * Intended for use only for testing, phpunit calls this before every test.
 773       */
 774      public static function reset_caches() {
 775          self::$propertiescache = null;
 776      }
 777  
 778      /**
 779       * Clean the user data.
 780       *
 781       * @param stdClass|array $user the user data to be validated against properties definition.
 782       * @return stdClass $user the cleaned user data.
 783       */
 784      public static function clean_data($user) {
 785          if (empty($user)) {
 786              return $user;
 787          }
 788  
 789          foreach ($user as $field => $value) {
 790              // Get the property parameter type and do the cleaning.
 791              try {
 792                  $user->$field = core_user::clean_field($value, $field);
 793              } catch (coding_exception $e) {
 794                  debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
 795              }
 796          }
 797  
 798          return $user;
 799      }
 800  
 801      /**
 802       * Clean a specific user field.
 803       *
 804       * @param string $data the user field data to be cleaned.
 805       * @param string $field the user field name on the property definition cache.
 806       * @return string the cleaned user data.
 807       */
 808      public static function clean_field($data, $field) {
 809          if (empty($data) || empty($field)) {
 810              return $data;
 811          }
 812  
 813          try {
 814              $type = core_user::get_property_type($field);
 815  
 816              if (isset(self::$propertiescache[$field]['choices'])) {
 817                  if (!array_key_exists($data, self::$propertiescache[$field]['choices'])) {
 818                      if (isset(self::$propertiescache[$field]['default'])) {
 819                          $data = self::$propertiescache[$field]['default'];
 820                      } else {
 821                          $data = '';
 822                      }
 823                  } else {
 824                      return $data;
 825                  }
 826              } else {
 827                  $data = clean_param($data, $type);
 828              }
 829          } catch (coding_exception $e) {
 830              debugging("The property '$field' could not be cleaned.", DEBUG_DEVELOPER);
 831          }
 832  
 833          return $data;
 834      }
 835  
 836      /**
 837       * Get the parameter type of the property.
 838       *
 839       * @param string $property property name to be retrieved.
 840       * @throws coding_exception if the requested property name is invalid.
 841       * @return int the property parameter type.
 842       */
 843      public static function get_property_type($property) {
 844  
 845          self::fill_properties_cache();
 846  
 847          if (!array_key_exists($property, self::$propertiescache)) {
 848              throw new coding_exception('Invalid property requested: ' . $property);
 849          }
 850  
 851          return self::$propertiescache[$property]['type'];
 852      }
 853  
 854      /**
 855       * Discover if the property is NULL_ALLOWED or NULL_NOT_ALLOWED.
 856       *
 857       * @param string $property property name to be retrieved.
 858       * @throws coding_exception if the requested property name is invalid.
 859       * @return bool true if the property is NULL_ALLOWED, false otherwise.
 860       */
 861      public static function get_property_null($property) {
 862  
 863          self::fill_properties_cache();
 864  
 865          if (!array_key_exists($property, self::$propertiescache)) {
 866              throw new coding_exception('Invalid property requested: ' . $property);
 867          }
 868  
 869          return self::$propertiescache[$property]['null'];
 870      }
 871  
 872      /**
 873       * Get the choices of the property.
 874       *
 875       * This is a helper method to validate a value against a list of acceptable choices.
 876       * For instance: country, language, themes and etc.
 877       *
 878       * @param string $property property name to be retrieved.
 879       * @throws coding_exception if the requested property name is invalid or if it does not has a list of choices.
 880       * @return array the property parameter type.
 881       */
 882      public static function get_property_choices($property) {
 883  
 884          self::fill_properties_cache();
 885  
 886          if (!array_key_exists($property, self::$propertiescache) && !array_key_exists('choices',
 887                  self::$propertiescache[$property])) {
 888  
 889              throw new coding_exception('Invalid property requested, or the property does not has a list of choices.');
 890          }
 891  
 892          return self::$propertiescache[$property]['choices'];
 893      }
 894  
 895      /**
 896       * Get the property default.
 897       *
 898       * This method gets the default value of a field (if exists).
 899       *
 900       * @param string $property property name to be retrieved.
 901       * @throws coding_exception if the requested property name is invalid or if it does not has a default value.
 902       * @return string the property default value.
 903       */
 904      public static function get_property_default($property) {
 905  
 906          self::fill_properties_cache();
 907  
 908          if (!array_key_exists($property, self::$propertiescache) || !isset(self::$propertiescache[$property]['default'])) {
 909              throw new coding_exception('Invalid property requested, or the property does not has a default value.');
 910          }
 911  
 912          return self::$propertiescache[$property]['default'];
 913      }
 914  
 915      /**
 916       * Definition of updateable user preferences and rules for data and access validation.
 917       *
 918       * array(
 919       *     'preferencename' => array(      // Either exact preference name or a regular expression.
 920       *          'null' => NULL_ALLOWED,    // Defaults to NULL_NOT_ALLOWED. Takes NULL_NOT_ALLOWED or NULL_ALLOWED.
 921       *          'type' => PARAM_TYPE,      // Expected parameter type of the user field - mandatory
 922       *          'choices' => array(1, 2..) // An array of accepted values of the user field - optional
 923       *          'default' => $CFG->setting // An default value for the field - optional
 924       *          'isregex' => false/true    // Whether the name of the preference is a regular expression (default false).
 925       *          'permissioncallback' => callable // Function accepting arguments ($user, $preferencename) that checks if current user
 926       *                                     // is allowed to modify this preference for given user.
 927       *                                     // If not specified core_user::default_preference_permission_check() will be assumed.
 928       *          'cleancallback' => callable // Custom callback for cleaning value if something more difficult than just type/choices is needed
 929       *                                     // accepts arguments ($value, $preferencename)
 930       *     )
 931       * )
 932       *
 933       * @return void
 934       */
 935      protected static function fill_preferences_cache() {
 936          if (self::$preferencescache !== null) {
 937              return;
 938          }
 939  
 940          // Array of user preferences and expected types/values.
 941          // Every preference that can be updated directly by user should be added here.
 942          $preferences = array();
 943          $preferences['auth_forcepasswordchange'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'choices' => array(0, 1),
 944              'permissioncallback' => function($user, $preferencename) {
 945                  global $USER;
 946                  $systemcontext = context_system::instance();
 947                  return ($USER->id != $user->id && (has_capability('moodle/user:update', $systemcontext) ||
 948                          ($user->timecreated > time() - 10 && has_capability('moodle/user:create', $systemcontext))));
 949              });
 950          $preferences['forum_markasreadonnotification'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
 951              'choices' => array(0, 1));
 952          $preferences['htmleditor'] = array('type' => PARAM_NOTAGS, 'null' => NULL_ALLOWED,
 953              'cleancallback' => function($value, $preferencename) {
 954                  if (empty($value) || !array_key_exists($value, core_component::get_plugin_list('editor'))) {
 955                      return null;
 956                  }
 957                  return $value;
 958              });
 959          $preferences['badgeprivacysetting'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 1,
 960              'choices' => array(0, 1), 'permissioncallback' => function($user, $preferencename) {
 961                  global $CFG, $USER;
 962                  return !empty($CFG->enablebadges) && $user->id == $USER->id;
 963              });
 964          $preferences['blogpagesize'] = array('type' => PARAM_INT, 'null' => NULL_NOT_ALLOWED, 'default' => 10,
 965              'permissioncallback' => function($user, $preferencename) {
 966                  global $USER;
 967                  return $USER->id == $user->id && has_capability('moodle/blog:view', context_system::instance());
 968              });
 969          $preferences['user_home_page_preference'] = array('type' => PARAM_INT, 'null' => NULL_ALLOWED, 'default' => HOMEPAGE_MY,
 970              'choices' => array(HOMEPAGE_SITE, HOMEPAGE_MY),
 971              'permissioncallback' => function ($user, $preferencename) {
 972                  global $CFG, $USER;
 973                  return $user->id == $USER->id &&
 974                      (!empty($CFG->defaulthomepage) && ($CFG->defaulthomepage == HOMEPAGE_USER));
 975              }
 976          );
 977  
 978          // Core components that may want to define their preferences.
 979          // List of core components implementing callback is hardcoded here for performance reasons.
 980          // TODO MDL-58184 cache list of core components implementing a function.
 981          $corecomponents = ['core_message', 'core_calendar', 'core_contentbank'];
 982          foreach ($corecomponents as $component) {
 983              if (($pluginpreferences = component_callback($component, 'user_preferences')) && is_array($pluginpreferences)) {
 984                  $preferences += $pluginpreferences;
 985              }
 986          }
 987  
 988          // Plugins that may define their preferences.
 989          if ($pluginsfunction = get_plugins_with_function('user_preferences')) {
 990              foreach ($pluginsfunction as $plugintype => $plugins) {
 991                  foreach ($plugins as $function) {
 992                      if (($pluginpreferences = call_user_func($function)) && is_array($pluginpreferences)) {
 993                          $preferences += $pluginpreferences;
 994                      }
 995                  }
 996              }
 997          }
 998  
 999          self::$preferencescache = $preferences;
1000      }
1001  
1002      /**
1003       * Retrieves the preference definition
1004       *
1005       * @param string $preferencename
1006       * @return array
1007       */
1008      protected static function get_preference_definition($preferencename) {
1009          self::fill_preferences_cache();
1010  
1011          foreach (self::$preferencescache as $key => $preference) {
1012              if (empty($preference['isregex'])) {
1013                  if ($key === $preferencename) {
1014                      return $preference;
1015                  }
1016              } else {
1017                  if (preg_match($key, $preferencename)) {
1018                      return $preference;
1019                  }
1020              }
1021          }
1022  
1023          throw new coding_exception('Invalid preference requested.');
1024      }
1025  
1026      /**
1027       * Default callback used for checking if current user is allowed to change permission of user $user
1028       *
1029       * @param stdClass $user
1030       * @param string $preferencename
1031       * @return bool
1032       */
1033      protected static function default_preference_permission_check($user, $preferencename) {
1034          global $USER;
1035          if (is_mnet_remote_user($user)) {
1036              // Can't edit MNET user.
1037              return false;
1038          }
1039  
1040          if ($user->id == $USER->id) {
1041              // Editing own profile.
1042              $systemcontext = context_system::instance();
1043              return has_capability('moodle/user:editownprofile', $systemcontext);
1044          } else  {
1045              // Teachers, parents, etc.
1046              $personalcontext = context_user::instance($user->id);
1047              if (!has_capability('moodle/user:editprofile', $personalcontext)) {
1048                  return false;
1049              }
1050              if (is_siteadmin($user->id) and !is_siteadmin($USER)) {
1051                  // Only admins may edit other admins.
1052                  return false;
1053              }
1054              return true;
1055          }
1056      }
1057  
1058      /**
1059       * Can current user edit preference of this/another user
1060       *
1061       * @param string $preferencename
1062       * @param stdClass $user
1063       * @return bool
1064       */
1065      public static function can_edit_preference($preferencename, $user) {
1066          if (!isloggedin() || isguestuser()) {
1067              // Guests can not edit anything.
1068              return false;
1069          }
1070  
1071          try {
1072              $definition = self::get_preference_definition($preferencename);
1073          } catch (coding_exception $e) {
1074              return false;
1075          }
1076  
1077          if ($user->deleted || !context_user::instance($user->id, IGNORE_MISSING)) {
1078              // User is deleted.
1079              return false;
1080          }
1081  
1082          if (isset($definition['permissioncallback'])) {
1083              $callback = $definition['permissioncallback'];
1084              if (is_callable($callback)) {
1085                  return call_user_func_array($callback, [$user, $preferencename]);
1086              } else {
1087                  throw new coding_exception('Permission callback for preference ' . s($preferencename) . ' is not callable');
1088                  return false;
1089              }
1090          } else {
1091              return self::default_preference_permission_check($user, $preferencename);
1092          }
1093      }
1094  
1095      /**
1096       * Clean value of a user preference
1097       *
1098       * @param string $value the user preference value to be cleaned.
1099       * @param string $preferencename the user preference name
1100       * @return string the cleaned preference value
1101       */
1102      public static function clean_preference($value, $preferencename) {
1103  
1104          $definition = self::get_preference_definition($preferencename);
1105  
1106          if (isset($definition['type']) && $value !== null) {
1107              $value = clean_param($value, $definition['type']);
1108          }
1109  
1110          if (isset($definition['cleancallback'])) {
1111              $callback = $definition['cleancallback'];
1112              if (is_callable($callback)) {
1113                  return $callback($value, $preferencename);
1114              } else {
1115                  throw new coding_exception('Clean callback for preference ' . s($preferencename) . ' is not callable');
1116              }
1117          } else if ($value === null && (!isset($definition['null']) || $definition['null'] == NULL_ALLOWED)) {
1118              return null;
1119          } else if (isset($definition['choices'])) {
1120              if (!in_array($value, $definition['choices'])) {
1121                  if (isset($definition['default'])) {
1122                      return $definition['default'];
1123                  } else {
1124                      $first = reset($definition['choices']);
1125                      return $first;
1126                  }
1127              } else {
1128                  return $value;
1129              }
1130          } else {
1131              if ($value === null) {
1132                  return isset($definition['default']) ? $definition['default'] : '';
1133              }
1134              return $value;
1135          }
1136      }
1137  
1138  }