Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 400 and 401]

   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   * Class process
  19   *
  20   * @package     tool_uploaduser
  21   * @copyright   2020 Moodle
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace tool_uploaduser;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use context_system;
  30  use context_coursecat;
  31  use core_course_category;
  32  
  33  use tool_uploaduser\local\field_value_validators;
  34  
  35  require_once($CFG->dirroot.'/user/profile/lib.php');
  36  require_once($CFG->dirroot.'/user/lib.php');
  37  require_once($CFG->dirroot.'/group/lib.php');
  38  require_once($CFG->dirroot.'/cohort/lib.php');
  39  require_once($CFG->libdir.'/csvlib.class.php');
  40  require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploaduser/locallib.php');
  41  
  42  /**
  43   * Process CSV file with users data, this will create/update users, enrol them into courses, etc
  44   *
  45   * @package     tool_uploaduser
  46   * @copyright   2020 Moodle
  47   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  48   */
  49  class process {
  50  
  51      /** @var \csv_import_reader  */
  52      protected $cir;
  53      /** @var \stdClass  */
  54      protected $formdata;
  55      /** @var \uu_progress_tracker  */
  56      protected $upt;
  57      /** @var array  */
  58      protected $filecolumns = null;
  59      /** @var int  */
  60      protected $today;
  61      /** @var \enrol_plugin|null */
  62      protected $manualenrol = null;
  63      /** @var array */
  64      protected $standardfields = [];
  65      /** @var array */
  66      protected $profilefields = [];
  67      /** @var \profile_field_base[] */
  68      protected $allprofilefields = [];
  69      /** @var string|\uu_progress_tracker|null  */
  70      protected $progresstrackerclass = null;
  71  
  72      /** @var int */
  73      protected $usersnew      = 0;
  74      /** @var int */
  75      protected $usersupdated  = 0;
  76      /** @var int /not printed yet anywhere */
  77      protected $usersuptodate = 0;
  78      /** @var int */
  79      protected $userserrors   = 0;
  80      /** @var int */
  81      protected $deletes       = 0;
  82      /** @var int */
  83      protected $deleteerrors  = 0;
  84      /** @var int */
  85      protected $renames       = 0;
  86      /** @var int */
  87      protected $renameerrors  = 0;
  88      /** @var int */
  89      protected $usersskipped  = 0;
  90      /** @var int */
  91      protected $weakpasswords = 0;
  92  
  93      /** @var array course cache - do not fetch all courses here, we  will not probably use them all anyway */
  94      protected $ccache         = [];
  95      /** @var array */
  96      protected $cohorts        = [];
  97      /** @var array  Course roles lookup cache. */
  98      protected $rolecache      = [];
  99      /** @var array System roles lookup cache. */
 100      protected $sysrolecache   = [];
 101      /** @var array cache of used manual enrol plugins in each course */
 102      protected $manualcache    = [];
 103      /** @var array officially supported plugins that are enabled */
 104      protected $supportedauths = [];
 105  
 106      /**
 107       * process constructor.
 108       *
 109       * @param \csv_import_reader $cir
 110       * @param string|null $progresstrackerclass
 111       * @throws \coding_exception
 112       */
 113      public function __construct(\csv_import_reader $cir, string $progresstrackerclass = null) {
 114          $this->cir = $cir;
 115          if ($progresstrackerclass) {
 116              if (!class_exists($progresstrackerclass) || !is_subclass_of($progresstrackerclass, \uu_progress_tracker::class)) {
 117                  throw new \coding_exception('Progress tracker class must extend \uu_progress_tracker');
 118              }
 119              $this->progresstrackerclass = $progresstrackerclass;
 120          } else {
 121              $this->progresstrackerclass = \uu_progress_tracker::class;
 122          }
 123  
 124          // Keep timestamp consistent.
 125          $today = time();
 126          $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0);
 127          $this->today = $today;
 128  
 129          $this->sysrolecache   = uu_allowed_sysroles_cache(); // System roles lookup cache.
 130          $this->supportedauths = uu_supported_auths(); // Officially supported plugins that are enabled.
 131  
 132          if (enrol_is_enabled('manual')) {
 133              // We use only manual enrol plugin here, if it is disabled no enrol is done.
 134              $this->manualenrol = enrol_get_plugin('manual');
 135          }
 136  
 137          $this->find_profile_fields();
 138          $this->find_standard_fields();
 139      }
 140  
 141      /**
 142       * Standard user fields.
 143       */
 144      protected function find_standard_fields(): void {
 145          $this->standardfields = array('id', 'username', 'email', 'emailstop',
 146              'city', 'country', 'lang', 'timezone', 'mailformat',
 147              'maildisplay', 'maildigest', 'htmleditor', 'autosubscribe',
 148              'institution', 'department', 'idnumber', 'phone1', 'phone2', 'address',
 149              'description', 'descriptionformat', 'password',
 150              'auth',        // Watch out when changing auth type or using external auth plugins!
 151              'oldusername', // Use when renaming users - this is the original username.
 152              'suspended',   // 1 means suspend user account, 0 means activate user account, nothing means keep as is.
 153              'theme',       // Define a theme for user when 'allowuserthemes' is enabled.
 154              'deleted',     // 1 means delete user
 155              'mnethostid',  // Can not be used for adding, updating or deleting of users - only for enrolments,
 156                             // groups, cohorts and suspending.
 157              'interests',
 158          );
 159          // Include all name fields.
 160          $this->standardfields = array_merge($this->standardfields, \core_user\fields::get_name_fields());
 161      }
 162  
 163      /**
 164       * Profile fields
 165       */
 166      protected function find_profile_fields(): void {
 167          global $CFG;
 168          require_once($CFG->dirroot . '/user/profile/lib.php');
 169          $this->allprofilefields = profile_get_user_fields_with_data(0);
 170          $this->profilefields = [];
 171          if ($proffields = $this->allprofilefields) {
 172              foreach ($proffields as $key => $proffield) {
 173                  $profilefieldname = 'profile_field_'.$proffield->get_shortname();
 174                  $this->profilefields[] = $profilefieldname;
 175                  // Re-index $proffields with key as shortname. This will be
 176                  // used while checking if profile data is key and needs to be converted (eg. menu profile field).
 177                  $proffields[$profilefieldname] = $proffield;
 178                  unset($proffields[$key]);
 179              }
 180              $this->allprofilefields = $proffields;
 181          }
 182      }
 183  
 184      /**
 185       * Returns the list of columns in the file
 186       *
 187       * @return array
 188       */
 189      public function get_file_columns(): array {
 190          if ($this->filecolumns === null) {
 191              $returnurl = new \moodle_url('/admin/tool/uploaduser/index.php');
 192              $this->filecolumns = uu_validate_user_upload_columns($this->cir,
 193                  $this->standardfields, $this->profilefields, $returnurl);
 194          }
 195          return $this->filecolumns;
 196      }
 197  
 198      /**
 199       * Set data from the form (or from CLI options)
 200       *
 201       * @param \stdClass $formdata
 202       */
 203      public function set_form_data(\stdClass $formdata): void {
 204          global $SESSION;
 205          $this->formdata = $formdata;
 206  
 207          // Clear bulk selection.
 208          if ($this->get_bulk()) {
 209              $SESSION->bulk_users = array();
 210          }
 211      }
 212  
 213      /**
 214       * Operation type
 215       * @return int
 216       */
 217      protected function get_operation_type(): int {
 218          return (int)$this->formdata->uutype;
 219      }
 220  
 221      /**
 222       * Setting to allow deletes
 223       * @return bool
 224       */
 225      protected function get_allow_deletes(): bool {
 226          $optype = $this->get_operation_type();
 227          return (!empty($this->formdata->uuallowdeletes) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC);
 228      }
 229  
 230      /**
 231       * Setting to allow matching user accounts on email
 232       * @return bool
 233       */
 234      protected function get_match_on_email(): bool {
 235          $optype = $this->get_operation_type();
 236          return (!empty($this->formdata->uumatchemail) && $optype != UU_USER_ADDNEW && $optype != UU_USER_ADDINC);
 237      }
 238  
 239      /**
 240       * Setting to allow deletes
 241       * @return bool
 242       */
 243      protected function get_allow_renames(): bool {
 244          $optype = $this->get_operation_type();
 245          return (!empty($this->formdata->uuallowrenames) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC);
 246      }
 247  
 248      /**
 249       * Setting to select for bulk actions (not available in CLI)
 250       * @return bool
 251       */
 252      public function get_bulk(): bool {
 253          return $this->formdata->uubulk ?? false;
 254      }
 255  
 256      /**
 257       * Setting for update type
 258       * @return int
 259       */
 260      protected function get_update_type(): int {
 261          return isset($this->formdata->uuupdatetype) ? $this->formdata->uuupdatetype : 0;
 262      }
 263  
 264      /**
 265       * Setting to allow update passwords
 266       * @return bool
 267       */
 268      protected function get_update_passwords(): bool {
 269          return !empty($this->formdata->uupasswordold)
 270              and $this->get_operation_type() != UU_USER_ADDNEW
 271              and $this->get_operation_type() != UU_USER_ADDINC
 272              and ($this->get_update_type() == UU_UPDATE_FILEOVERRIDE or $this->get_update_type() == UU_UPDATE_ALLOVERRIDE);
 273      }
 274  
 275      /**
 276       * Setting to allow email duplicates
 277       * @return bool
 278       */
 279      protected function get_allow_email_duplicates(): bool {
 280          global $CFG;
 281          return !(empty($CFG->allowaccountssameemail) ? 1 : $this->formdata->uunoemailduplicates);
 282      }
 283  
 284      /**
 285       * Setting for reset password
 286       * @return int UU_PWRESET_NONE, UU_PWRESET_WEAK, UU_PWRESET_ALL
 287       */
 288      protected function get_reset_passwords(): int {
 289          return isset($this->formdata->uuforcepasswordchange) ? $this->formdata->uuforcepasswordchange : UU_PWRESET_NONE;
 290      }
 291  
 292      /**
 293       * Setting to allow create passwords
 294       * @return bool
 295       */
 296      protected function get_create_paswords(): bool {
 297          return (!empty($this->formdata->uupasswordnew) and $this->get_operation_type() != UU_USER_UPDATE);
 298      }
 299  
 300      /**
 301       * Setting to allow suspends
 302       * @return bool
 303       */
 304      protected function get_allow_suspends(): bool {
 305          return !empty($this->formdata->uuallowsuspends);
 306      }
 307  
 308      /**
 309       * Setting to normalise user names
 310       * @return bool
 311       */
 312      protected function get_normalise_user_names(): bool {
 313          return !empty($this->formdata->uustandardusernames);
 314      }
 315  
 316      /**
 317       * Helper method to return Yes/No string
 318       *
 319       * @param bool $value
 320       * @return string
 321       */
 322      protected function get_string_yes_no($value): string {
 323          return $value ? get_string('yes') : get_string('no');
 324      }
 325  
 326      /**
 327       * Process the CSV file
 328       */
 329      public function process() {
 330          // Init csv import helper.
 331          $this->cir->init();
 332  
 333          $classname = $this->progresstrackerclass;
 334          $this->upt = new $classname();
 335          $this->upt->start(); // Start table.
 336  
 337          $linenum = 1; // Column header is first line.
 338          while ($line = $this->cir->next()) {
 339              $this->upt->flush();
 340              $linenum++;
 341  
 342              $this->upt->track('line', $linenum);
 343              $this->process_line($line);
 344          }
 345  
 346          $this->upt->close(); // Close table.
 347          $this->cir->close();
 348          $this->cir->cleanup(true);
 349      }
 350  
 351      /**
 352       * Prepare one line from CSV file as a user record
 353       *
 354       * @param array $line
 355       * @return \stdClass|null
 356       */
 357      protected function prepare_user_record(array $line): ?\stdClass {
 358          global $CFG, $USER;
 359  
 360          $user = new \stdClass();
 361  
 362          // Add fields to user object.
 363          foreach ($line as $keynum => $value) {
 364              if (!isset($this->get_file_columns()[$keynum])) {
 365                  // This should not happen.
 366                  continue;
 367              }
 368              $key = $this->get_file_columns()[$keynum];
 369              if (strpos($key, 'profile_field_') === 0) {
 370                  // NOTE: bloody mega hack alert!!
 371                  if (isset($USER->$key) and is_array($USER->$key)) {
 372                      // This must be some hacky field that is abusing arrays to store content and format.
 373                      $user->$key = array();
 374                      $user->{$key['text']}   = $value;
 375                      $user->{$key['format']} = FORMAT_MOODLE;
 376                  } else {
 377                      $user->$key = trim($value);
 378                  }
 379              } else {
 380                  $user->$key = trim($value);
 381              }
 382  
 383              if (in_array($key, $this->upt->columns)) {
 384                  // Default value in progress tracking table, can be changed later.
 385                  $this->upt->track($key, s($value), 'normal');
 386              }
 387          }
 388          if (!isset($user->username)) {
 389              // Prevent warnings below.
 390              $user->username = '';
 391          }
 392  
 393          if ($this->get_operation_type() == UU_USER_ADDNEW or $this->get_operation_type() == UU_USER_ADDINC) {
 394              // User creation is a special case - the username may be constructed from templates using firstname and lastname
 395              // better never try this in mixed update types.
 396              $error = false;
 397              if (!isset($user->firstname) or $user->firstname === '') {
 398                  $this->upt->track('status', get_string('missingfield', 'error', 'firstname'), 'error');
 399                  $this->upt->track('firstname', get_string('error'), 'error');
 400                  $error = true;
 401              }
 402              if (!isset($user->lastname) or $user->lastname === '') {
 403                  $this->upt->track('status', get_string('missingfield', 'error', 'lastname'), 'error');
 404                  $this->upt->track('lastname', get_string('error'), 'error');
 405                  $error = true;
 406              }
 407              if ($error) {
 408                  $this->userserrors++;
 409                  return null;
 410              }
 411              // We require username too - we might use template for it though.
 412              if (empty($user->username) and !empty($this->formdata->username)) {
 413                  $user->username = uu_process_template($this->formdata->username, $user);
 414                  $this->upt->track('username', s($user->username));
 415              }
 416          }
 417  
 418          // Normalize username.
 419          $user->originalusername = $user->username;
 420          if ($this->get_normalise_user_names()) {
 421              $user->username = \core_user::clean_field($user->username, 'username');
 422          }
 423  
 424          // Make sure we really have username.
 425          if (empty($user->username) && !$this->get_match_on_email()) {
 426              $this->upt->track('status', get_string('missingfield', 'error', 'username'), 'error');
 427              $this->upt->track('username', get_string('error'), 'error');
 428              $this->userserrors++;
 429              return null;
 430          } else if ($user->username === 'guest') {
 431              $this->upt->track('status', get_string('guestnoeditprofileother', 'error'), 'error');
 432              $this->userserrors++;
 433              return null;
 434          }
 435  
 436          if ($user->username !== \core_user::clean_field($user->username, 'username')) {
 437              $this->upt->track('status', get_string('invalidusername', 'error', 'username'), 'error');
 438              $this->upt->track('username', get_string('error'), 'error');
 439              $this->userserrors++;
 440          }
 441  
 442          if (empty($user->mnethostid)) {
 443              $user->mnethostid = $CFG->mnet_localhost_id;
 444          }
 445  
 446          return $user;
 447      }
 448  
 449      /**
 450       * Process one line from CSV file
 451       *
 452       * @param array $line
 453       * @throws \coding_exception
 454       * @throws \dml_exception
 455       * @throws \moodle_exception
 456       */
 457      public function process_line(array $line) {
 458          global $DB, $CFG, $SESSION;
 459  
 460          if (!$user = $this->prepare_user_record($line)) {
 461              return;
 462          }
 463  
 464          $matchparam = $this->get_match_on_email() ? ['email' => $user->email] : ['username' => $user->username];
 465          if ($existinguser = $DB->get_records('user', $matchparam + ['mnethostid' => $user->mnethostid])) {
 466              if (is_array($existinguser) && count($existinguser) !== 1) {
 467                  $this->upt->track('status', get_string('duplicateemail', 'tool_uploaduser', $user->email), 'warning');
 468                  $this->userserrors++;
 469                  return;
 470  
 471              }
 472  
 473              $existinguser = is_array($existinguser) ? array_values($existinguser)[0] : $existinguser;
 474              $this->upt->track('id', $existinguser->id, 'normal', false);
 475          }
 476  
 477          if ($user->mnethostid == $CFG->mnet_localhost_id) {
 478              $remoteuser = false;
 479  
 480              // Find out if username incrementing required.
 481              if ($existinguser and $this->get_operation_type() == UU_USER_ADDINC) {
 482                  $user->username = uu_increment_username($user->username);
 483                  $existinguser = false;
 484              }
 485  
 486          } else {
 487              if (!$existinguser or $this->get_operation_type() == UU_USER_ADDINC) {
 488                  $this->upt->track('status', get_string('errormnetadd', 'tool_uploaduser'), 'error');
 489                  $this->userserrors++;
 490                  return;
 491              }
 492  
 493              $remoteuser = true;
 494  
 495              // Make sure there are no changes of existing fields except the suspended status.
 496              foreach ((array)$existinguser as $k => $v) {
 497                  if ($k === 'suspended') {
 498                      continue;
 499                  }
 500                  if (property_exists($user, $k)) {
 501                      $user->$k = $v;
 502                  }
 503                  if (in_array($k, $this->upt->columns)) {
 504                      if ($k === 'password' or $k === 'oldusername' or $k === 'deleted') {
 505                          $this->upt->track($k, '', 'normal', false);
 506                      } else {
 507                          $this->upt->track($k, s($v), 'normal', false);
 508                      }
 509                  }
 510              }
 511              unset($user->oldusername);
 512              unset($user->password);
 513              $user->auth = $existinguser->auth;
 514          }
 515  
 516          // Notify about nay username changes.
 517          if ($user->originalusername !== $user->username) {
 518              $this->upt->track('username', '', 'normal', false); // Clear previous.
 519              $this->upt->track('username', s($user->originalusername).'-->'.s($user->username), 'info');
 520          } else {
 521              $this->upt->track('username', s($user->username), 'normal', false);
 522          }
 523          unset($user->originalusername);
 524  
 525          // Verify if the theme is valid and allowed to be set.
 526          if (isset($user->theme)) {
 527              list($status, $message) = field_value_validators::validate_theme($user->theme);
 528              if ($status !== 'normal' && !empty($message)) {
 529                  $this->upt->track('status', $message, $status);
 530                  // Unset the theme when validation fails.
 531                  unset($user->theme);
 532              }
 533          }
 534  
 535          // Add default values for remaining fields.
 536          $formdefaults = array();
 537          if (!$existinguser ||
 538                  ($this->get_update_type() != UU_UPDATE_FILEOVERRIDE && $this->get_update_type() != UU_UPDATE_NOCHANGES)) {
 539              foreach ($this->standardfields as $field) {
 540                  if (isset($user->$field)) {
 541                      continue;
 542                  }
 543                  // All validation moved to form2.
 544                  if (isset($this->formdata->$field)) {
 545                      // Process templates.
 546                      $user->$field = uu_process_template($this->formdata->$field, $user);
 547                      $formdefaults[$field] = true;
 548                      if (in_array($field, $this->upt->columns)) {
 549                          $this->upt->track($field, s($user->$field), 'normal');
 550                      }
 551                  }
 552              }
 553              foreach ($this->allprofilefields as $field => $profilefield) {
 554                  if (isset($user->$field)) {
 555                      continue;
 556                  }
 557                  if (isset($this->formdata->$field)) {
 558                      // Process templates.
 559                      $user->$field = uu_process_template($this->formdata->$field, $user);
 560  
 561                      // Form contains key and later code expects value.
 562                      // Convert key to value for required profile fields.
 563                      if (method_exists($profilefield, 'convert_external_data')) {
 564                          $user->$field = $profilefield->edit_save_data_preprocess($user->$field, null);
 565                      }
 566  
 567                      $formdefaults[$field] = true;
 568                  }
 569              }
 570          }
 571  
 572          // Delete user.
 573          if (!empty($user->deleted)) {
 574              if (!$this->get_allow_deletes() or $remoteuser or
 575                      !has_capability('moodle/user:delete', context_system::instance())) {
 576                  $this->usersskipped++;
 577                  $this->upt->track('status', get_string('usernotdeletedoff', 'error'), 'warning');
 578                  return;
 579              }
 580              if ($existinguser) {
 581                  if (is_siteadmin($existinguser->id)) {
 582                      $this->upt->track('status', get_string('usernotdeletedadmin', 'error'), 'error');
 583                      $this->deleteerrors++;
 584                      return;
 585                  }
 586                  if (delete_user($existinguser)) {
 587                      $this->upt->track('status', get_string('userdeleted', 'tool_uploaduser'));
 588                      $this->deletes++;
 589                  } else {
 590                      $this->upt->track('status', get_string('usernotdeletederror', 'error'), 'error');
 591                      $this->deleteerrors++;
 592                  }
 593              } else {
 594                  $this->upt->track('status', get_string('usernotdeletedmissing', 'error'), 'error');
 595                  $this->deleteerrors++;
 596              }
 597              return;
 598          }
 599          // We do not need the deleted flag anymore.
 600          unset($user->deleted);
 601  
 602          $matchonemailallowrename = $this->get_match_on_email() && $this->get_allow_renames();
 603          if ($matchonemailallowrename && $user->username && ($user->username !== $existinguser->username)) {
 604              $user->oldusername = $existinguser->username;
 605              $existinguser = false;
 606          }
 607  
 608          // Renaming requested?
 609          if (!empty($user->oldusername) ) {
 610              if (!$this->get_allow_renames()) {
 611                  $this->usersskipped++;
 612                  $this->upt->track('status', get_string('usernotrenamedoff', 'error'), 'warning');
 613                  return;
 614              }
 615  
 616              if ($existinguser) {
 617                  $this->upt->track('status', get_string('usernotrenamedexists', 'error'), 'error');
 618                  $this->renameerrors++;
 619                  return;
 620              }
 621  
 622              if ($user->username === 'guest') {
 623                  $this->upt->track('status', get_string('guestnoeditprofileother', 'error'), 'error');
 624                  $this->renameerrors++;
 625                  return;
 626              }
 627  
 628              if ($this->get_normalise_user_names()) {
 629                  $oldusername = \core_user::clean_field($user->oldusername, 'username');
 630              } else {
 631                  $oldusername = $user->oldusername;
 632              }
 633  
 634              // No guessing when looking for old username, it must be exact match.
 635              if ($olduser = $DB->get_record('user',
 636                      ['username' => $oldusername, 'mnethostid' => $CFG->mnet_localhost_id])) {
 637                  $this->upt->track('id', $olduser->id, 'normal', false);
 638                  if (is_siteadmin($olduser->id)) {
 639                      $this->upt->track('status', get_string('usernotrenamedadmin', 'error'), 'error');
 640                      $this->renameerrors++;
 641                      return;
 642                  }
 643                  $DB->set_field('user', 'username', $user->username, ['id' => $olduser->id]);
 644                  $this->upt->track('username', '', 'normal', false); // Clear previous.
 645                  $this->upt->track('username', s($oldusername).'-->'.s($user->username), 'info');
 646                  $this->upt->track('status', get_string('userrenamed', 'tool_uploaduser'));
 647                  $this->renames++;
 648              } else {
 649                  $this->upt->track('status', get_string('usernotrenamedmissing', 'error'), 'error');
 650                  $this->renameerrors++;
 651                  return;
 652              }
 653              $existinguser = $olduser;
 654              $existinguser->username = $user->username;
 655          }
 656  
 657          // Can we process with update or insert?
 658          $skip = false;
 659          switch ($this->get_operation_type()) {
 660              case UU_USER_ADDNEW:
 661                  if ($existinguser) {
 662                      $this->usersskipped++;
 663                      $this->upt->track('status', get_string('usernotaddedregistered', 'error'), 'warning');
 664                      $skip = true;
 665                  }
 666                  break;
 667  
 668              case UU_USER_ADDINC:
 669                  if ($existinguser) {
 670                      // This should not happen!
 671                      $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error');
 672                      $this->userserrors++;
 673                      $skip = true;
 674                  }
 675                  break;
 676  
 677              case UU_USER_ADD_UPDATE:
 678                  break;
 679  
 680              case UU_USER_UPDATE:
 681                  if (!$existinguser) {
 682                      $this->usersskipped++;
 683                      $this->upt->track('status', get_string('usernotupdatednotexists', 'error'), 'warning');
 684                      $skip = true;
 685                  }
 686                  break;
 687  
 688              default:
 689                  // Unknown type.
 690                  $skip = true;
 691          }
 692  
 693          if ($skip) {
 694              return;
 695          }
 696  
 697          if ($existinguser) {
 698              $user->id = $existinguser->id;
 699  
 700              $this->upt->track('username', \html_writer::link(
 701                  new \moodle_url('/user/profile.php', ['id' => $existinguser->id]), s($existinguser->username)), 'normal', false);
 702              $this->upt->track('suspended', $this->get_string_yes_no($existinguser->suspended) , 'normal', false);
 703              $this->upt->track('auth', $existinguser->auth, 'normal', false);
 704  
 705              if (is_siteadmin($user->id)) {
 706                  $this->upt->track('status', get_string('usernotupdatedadmin', 'error'), 'error');
 707                  $this->userserrors++;
 708                  return;
 709              }
 710  
 711              $existinguser->timemodified = time();
 712              // Do NOT mess with timecreated or firstaccess here!
 713  
 714              // Load existing profile data.
 715              profile_load_data($existinguser);
 716  
 717              $doupdate = false;
 718              $dologout = false;
 719  
 720              if ($this->get_update_type() != UU_UPDATE_NOCHANGES and !$remoteuser) {
 721  
 722                  // Handle 'auth' column separately, the field can never be missing from a user.
 723                  if (!empty($user->auth) && ($user->auth !== $existinguser->auth) &&
 724                          ($this->get_update_type() != UU_UPDATE_MISSING)) {
 725  
 726                      $this->upt->track('auth', s($existinguser->auth).'-->'.s($user->auth), 'info', false);
 727                      $existinguser->auth = $user->auth;
 728                      if (!isset($this->supportedauths[$user->auth])) {
 729                          $this->upt->track('auth', get_string('userauthunsupported', 'error'), 'warning');
 730                      }
 731                      $doupdate = true;
 732                      if ($existinguser->auth === 'nologin') {
 733                          $dologout = true;
 734                      }
 735                  }
 736                  $allcolumns = array_merge($this->standardfields, $this->profilefields);
 737                  foreach ($allcolumns as $column) {
 738                      if ($column === 'username' or $column === 'password' or $column === 'auth' or $column === 'suspended') {
 739                          // These can not be changed here.
 740                          continue;
 741                      }
 742                      if (!property_exists($user, $column) or !property_exists($existinguser, $column)) {
 743                          continue;
 744                      }
 745                      if ($this->get_update_type() == UU_UPDATE_MISSING) {
 746                          if (!is_null($existinguser->$column) and $existinguser->$column !== '') {
 747                              continue;
 748                          }
 749                      } else if ($this->get_update_type() == UU_UPDATE_ALLOVERRIDE) {
 750                          // We override everything.
 751                          null;
 752                      } else if ($this->get_update_type() == UU_UPDATE_FILEOVERRIDE) {
 753                          if (!empty($formdefaults[$column])) {
 754                              // Do not override with form defaults.
 755                              continue;
 756                          }
 757                      }
 758                      if ($existinguser->$column !== $user->$column) {
 759                          if ($column === 'email') {
 760                              $select = $DB->sql_like('email', ':email', false, true, false, '|');
 761                              $params = array('email' => $DB->sql_like_escape($user->email, '|'));
 762                              if ($DB->record_exists_select('user', $select , $params)) {
 763  
 764                                  $changeincase = \core_text::strtolower($existinguser->$column) === \core_text::strtolower(
 765                                          $user->$column);
 766  
 767                                  if ($changeincase) {
 768                                      // If only case is different then switch to lower case and carry on.
 769                                      $user->$column = \core_text::strtolower($user->$column);
 770                                      continue;
 771                                  } else if (!$this->get_allow_email_duplicates()) {
 772                                      $this->upt->track('email', get_string('useremailduplicate', 'error'), 'error');
 773                                      $this->upt->track('status', get_string('usernotupdatederror', 'error'), 'error');
 774                                      $this->userserrors++;
 775                                      return;
 776                                  } else {
 777                                      $this->upt->track('email', get_string('useremailduplicate', 'error'), 'warning');
 778                                  }
 779                              }
 780                              if (!validate_email($user->email)) {
 781                                  $this->upt->track('email', get_string('invalidemail'), 'warning');
 782                              }
 783                          }
 784  
 785                          if ($column === 'lang') {
 786                              if (empty($user->lang)) {
 787                                  // Do not change to not-set value.
 788                                  continue;
 789                              } else if (\core_user::clean_field($user->lang, 'lang') === '') {
 790                                  $this->upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning');
 791                                  continue;
 792                              }
 793                          }
 794  
 795                          if (in_array($column, $this->upt->columns)) {
 796                              $this->upt->track($column, s($existinguser->$column).'-->'.s($user->$column), 'info', false);
 797                          }
 798                          $existinguser->$column = $user->$column;
 799                          $doupdate = true;
 800                      }
 801                  }
 802              }
 803  
 804              try {
 805                  $auth = get_auth_plugin($existinguser->auth);
 806              } catch (\Exception $e) {
 807                  $this->upt->track('auth', get_string('userautherror', 'error', s($existinguser->auth)), 'error');
 808                  $this->upt->track('status', get_string('usernotupdatederror', 'error'), 'error');
 809                  $this->userserrors++;
 810                  return;
 811              }
 812              $isinternalauth = $auth->is_internal();
 813  
 814              // Deal with suspending and activating of accounts.
 815              if ($this->get_allow_suspends() and isset($user->suspended) and $user->suspended !== '') {
 816                  $user->suspended = $user->suspended ? 1 : 0;
 817                  if ($existinguser->suspended != $user->suspended) {
 818                      $this->upt->track('suspended', '', 'normal', false);
 819                      $this->upt->track('suspended',
 820                          $this->get_string_yes_no($existinguser->suspended).'-->'.$this->get_string_yes_no($user->suspended),
 821                          'info', false);
 822                      $existinguser->suspended = $user->suspended;
 823                      $doupdate = true;
 824                      if ($existinguser->suspended) {
 825                          $dologout = true;
 826                      }
 827                  }
 828              }
 829  
 830              // Changing of passwords is a special case
 831              // do not force password changes for external auth plugins!
 832              $oldpw = $existinguser->password;
 833  
 834              if ($remoteuser) {
 835                  // Do not mess with passwords of remote users.
 836                  null;
 837              } else if (!$isinternalauth) {
 838                  $existinguser->password = AUTH_PASSWORD_NOT_CACHED;
 839                  $this->upt->track('password', '-', 'normal', false);
 840                  // Clean up prefs.
 841                  unset_user_preference('create_password', $existinguser);
 842                  unset_user_preference('auth_forcepasswordchange', $existinguser);
 843  
 844              } else if (!empty($user->password)) {
 845                  if ($this->get_update_passwords()) {
 846                      // Check for passwords that we want to force users to reset next
 847                      // time they log in.
 848                      $errmsg = null;
 849                      $weak = !check_password_policy($user->password, $errmsg, $user);
 850                      if ($this->get_reset_passwords() == UU_PWRESET_ALL or
 851                              ($this->get_reset_passwords() == UU_PWRESET_WEAK and $weak)) {
 852                          if ($weak) {
 853                              $this->weakpasswords++;
 854                              $this->upt->track('password', get_string('invalidpasswordpolicy', 'error'), 'warning');
 855                          }
 856                          set_user_preference('auth_forcepasswordchange', 1, $existinguser);
 857                      } else {
 858                          unset_user_preference('auth_forcepasswordchange', $existinguser);
 859                      }
 860                      unset_user_preference('create_password', $existinguser); // No need to create password any more.
 861  
 862                      // Use a low cost factor when generating bcrypt hash otherwise
 863                      // hashing would be slow when uploading lots of users. Hashes
 864                      // will be automatically updated to a higher cost factor the first
 865                      // time the user logs in.
 866                      $existinguser->password = hash_internal_user_password($user->password, true);
 867                      $this->upt->track('password', $user->password, 'normal', false);
 868                  } else {
 869                      // Do not print password when not changed.
 870                      $this->upt->track('password', '', 'normal', false);
 871                  }
 872              }
 873  
 874              if ($doupdate or $existinguser->password !== $oldpw) {
 875                  // We want only users that were really updated.
 876                  user_update_user($existinguser, false, false);
 877  
 878                  $this->upt->track('status', get_string('useraccountupdated', 'tool_uploaduser'));
 879                  $this->usersupdated++;
 880  
 881                  if (!$remoteuser) {
 882                      // Pre-process custom profile menu fields data from csv file.
 883                      $existinguser = uu_pre_process_custom_profile_data($existinguser);
 884                      // Save custom profile fields data from csv file.
 885                      profile_save_data($existinguser);
 886                  }
 887  
 888                  if ($this->get_bulk() == UU_BULK_UPDATED or $this->get_bulk() == UU_BULK_ALL) {
 889                      if (!in_array($user->id, $SESSION->bulk_users)) {
 890                          $SESSION->bulk_users[] = $user->id;
 891                      }
 892                  }
 893  
 894                  // Trigger event.
 895                  \core\event\user_updated::create_from_userid($existinguser->id)->trigger();
 896  
 897              } else {
 898                  // No user information changed.
 899                  $this->upt->track('status', get_string('useraccountuptodate', 'tool_uploaduser'));
 900                  $this->usersuptodate++;
 901  
 902                  if ($this->get_bulk() == UU_BULK_ALL) {
 903                      if (!in_array($user->id, $SESSION->bulk_users)) {
 904                          $SESSION->bulk_users[] = $user->id;
 905                      }
 906                  }
 907              }
 908  
 909              if ($dologout) {
 910                  \core\session\manager::kill_user_sessions($existinguser->id);
 911              }
 912  
 913          } else {
 914              // Save the new user to the database.
 915              $user->confirmed    = 1;
 916              $user->timemodified = time();
 917              $user->timecreated  = time();
 918              $user->mnethostid   = $CFG->mnet_localhost_id; // We support ONLY local accounts here, sorry.
 919  
 920              if (!isset($user->suspended) or $user->suspended === '') {
 921                  $user->suspended = 0;
 922              } else {
 923                  $user->suspended = $user->suspended ? 1 : 0;
 924              }
 925              $this->upt->track('suspended', $this->get_string_yes_no($user->suspended), 'normal', false);
 926  
 927              if (empty($user->auth)) {
 928                  $user->auth = 'manual';
 929              }
 930              $this->upt->track('auth', $user->auth, 'normal', false);
 931  
 932              // Do not insert record if new auth plugin does not exist!
 933              try {
 934                  $auth = get_auth_plugin($user->auth);
 935              } catch (\Exception $e) {
 936                  $this->upt->track('auth', get_string('userautherror', 'error', s($user->auth)), 'error');
 937                  $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error');
 938                  $this->userserrors++;
 939                  return;
 940              }
 941              if (!isset($this->supportedauths[$user->auth])) {
 942                  $this->upt->track('auth', get_string('userauthunsupported', 'error'), 'warning');
 943              }
 944  
 945              $isinternalauth = $auth->is_internal();
 946  
 947              if (empty($user->email)) {
 948                  $this->upt->track('email', get_string('invalidemail'), 'error');
 949                  $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error');
 950                  $this->userserrors++;
 951                  return;
 952  
 953              } else if ($DB->record_exists('user', ['email' => $user->email])) {
 954                  if (!$this->get_allow_email_duplicates()) {
 955                      $this->upt->track('email', get_string('useremailduplicate', 'error'), 'error');
 956                      $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error');
 957                      $this->userserrors++;
 958                      return;
 959                  } else {
 960                      $this->upt->track('email', get_string('useremailduplicate', 'error'), 'warning');
 961                  }
 962              }
 963              if (!validate_email($user->email)) {
 964                  $this->upt->track('email', get_string('invalidemail'), 'warning');
 965              }
 966  
 967              if (empty($user->lang)) {
 968                  $user->lang = '';
 969              } else if (\core_user::clean_field($user->lang, 'lang') === '') {
 970                  $this->upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning');
 971                  $user->lang = '';
 972              }
 973  
 974              $forcechangepassword = false;
 975  
 976              if ($isinternalauth) {
 977                  if (empty($user->password)) {
 978                      if ($this->get_create_paswords()) {
 979                          $user->password = 'to be generated';
 980                          $this->upt->track('password', '', 'normal', false);
 981                          $this->upt->track('password', get_string('uupasswordcron', 'tool_uploaduser'), 'warning', false);
 982                      } else {
 983                          $this->upt->track('password', '', 'normal', false);
 984                          $this->upt->track('password', get_string('missingfield', 'error', 'password'), 'error');
 985                          $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error');
 986                          $this->userserrors++;
 987                          return;
 988                      }
 989                  } else {
 990                      $errmsg = null;
 991                      $weak = !check_password_policy($user->password, $errmsg, $user);
 992                      if ($this->get_reset_passwords() == UU_PWRESET_ALL or
 993                              ($this->get_reset_passwords() == UU_PWRESET_WEAK and $weak)) {
 994                          if ($weak) {
 995                              $this->weakpasswords++;
 996                              $this->upt->track('password', get_string('invalidpasswordpolicy', 'error'), 'warning');
 997                          }
 998                          $forcechangepassword = true;
 999                      }
1000                      // Use a low cost factor when generating bcrypt hash otherwise
1001                      // hashing would be slow when uploading lots of users. Hashes
1002                      // will be automatically updated to a higher cost factor the first
1003                      // time the user logs in.
1004                      $user->password = hash_internal_user_password($user->password, true);
1005                  }
1006              } else {
1007                  $user->password = AUTH_PASSWORD_NOT_CACHED;
1008                  $this->upt->track('password', '-', 'normal', false);
1009              }
1010  
1011              $user->id = user_create_user($user, false, false);
1012              $this->upt->track('username', \html_writer::link(
1013                  new \moodle_url('/user/profile.php', ['id' => $user->id]), s($user->username)), 'normal', false);
1014  
1015              // Pre-process custom profile menu fields data from csv file.
1016              $user = uu_pre_process_custom_profile_data($user);
1017              // Save custom profile fields data.
1018              profile_save_data($user);
1019  
1020              if ($forcechangepassword) {
1021                  set_user_preference('auth_forcepasswordchange', 1, $user);
1022              }
1023              if ($user->password === 'to be generated') {
1024                  set_user_preference('create_password', 1, $user);
1025              }
1026  
1027              // Trigger event.
1028              \core\event\user_created::create_from_userid($user->id)->trigger();
1029  
1030              $this->upt->track('status', get_string('newuser'));
1031              $this->upt->track('id', $user->id, 'normal', false);
1032              $this->usersnew++;
1033  
1034              // Make sure user context exists.
1035              \context_user::instance($user->id);
1036  
1037              if ($this->get_bulk() == UU_BULK_NEW or $this->get_bulk() == UU_BULK_ALL) {
1038                  if (!in_array($user->id, $SESSION->bulk_users)) {
1039                      $SESSION->bulk_users[] = $user->id;
1040                  }
1041              }
1042          }
1043  
1044          // Update user interests.
1045          if (isset($user->interests) && strval($user->interests) !== '') {
1046              useredit_update_interests($user, preg_split('/\s*,\s*/', $user->interests, -1, PREG_SPLIT_NO_EMPTY));
1047          }
1048  
1049          // Add to cohort first, it might trigger enrolments indirectly - do NOT create cohorts here!
1050          foreach ($this->get_file_columns() as $column) {
1051              if (!preg_match('/^cohort\d+$/', $column)) {
1052                  continue;
1053              }
1054  
1055              if (!empty($user->$column)) {
1056                  $addcohort = $user->$column;
1057                  if (!isset($this->cohorts[$addcohort])) {
1058                      if (is_number($addcohort)) {
1059                          // Only non-numeric idnumbers!
1060                          $cohort = $DB->get_record('cohort', ['id' => $addcohort]);
1061                      } else {
1062                          $cohort = $DB->get_record('cohort', ['idnumber' => $addcohort]);
1063                          if (empty($cohort) && has_capability('moodle/cohort:manage', \context_system::instance())) {
1064                              // Cohort was not found. Create a new one.
1065                              $cohortid = cohort_add_cohort((object)array(
1066                                  'idnumber' => $addcohort,
1067                                  'name' => $addcohort,
1068                                  'contextid' => \context_system::instance()->id
1069                              ));
1070                              $cohort = $DB->get_record('cohort', ['id' => $cohortid]);
1071                          }
1072                      }
1073  
1074                      if (empty($cohort)) {
1075                          $this->cohorts[$addcohort] = get_string('unknowncohort', 'core_cohort', s($addcohort));
1076                      } else if (!empty($cohort->component)) {
1077                          // Cohorts synchronised with external sources must not be modified!
1078                          $this->cohorts[$addcohort] = get_string('external', 'core_cohort');
1079                      } else {
1080                          $this->cohorts[$addcohort] = $cohort;
1081                      }
1082                  }
1083  
1084                  if (is_object($this->cohorts[$addcohort])) {
1085                      $cohort = $this->cohorts[$addcohort];
1086                      if (!$DB->record_exists('cohort_members', ['cohortid' => $cohort->id, 'userid' => $user->id])) {
1087                          cohort_add_member($cohort->id, $user->id);
1088                          // We might add special column later, for now let's abuse enrolments.
1089                          $this->upt->track('enrolments', get_string('useradded', 'core_cohort', s($cohort->name)), 'info');
1090                      }
1091                  } else {
1092                      // Error message.
1093                      $this->upt->track('enrolments', $this->cohorts[$addcohort], 'error');
1094                  }
1095              }
1096          }
1097  
1098          // Find course enrolments, groups, roles/types and enrol periods
1099          // this is again a special case, we always do this for any updated or created users.
1100          foreach ($this->get_file_columns() as $column) {
1101              if (preg_match('/^sysrole\d+$/', $column)) {
1102  
1103                  if (!empty($user->$column)) {
1104                      $sysrolename = $user->$column;
1105                      if ($sysrolename[0] == '-') {
1106                          $removing = true;
1107                          $sysrolename = substr($sysrolename, 1);
1108                      } else {
1109                          $removing = false;
1110                      }
1111  
1112                      if (array_key_exists($sysrolename, $this->sysrolecache)) {
1113                          $sysroleid = $this->sysrolecache[$sysrolename]->id;
1114                      } else {
1115                          $this->upt->track('enrolments', get_string('unknownrole', 'error', s($sysrolename)), 'error');
1116                          continue;
1117                      }
1118  
1119                      if ($removing) {
1120                          if (user_has_role_assignment($user->id, $sysroleid, SYSCONTEXTID)) {
1121                              role_unassign($sysroleid, $user->id, SYSCONTEXTID);
1122                              $this->upt->track('enrolments', get_string('unassignedsysrole',
1123                                  'tool_uploaduser', $this->sysrolecache[$sysroleid]->name), 'info');
1124                          }
1125                      } else {
1126                          if (!user_has_role_assignment($user->id, $sysroleid, SYSCONTEXTID)) {
1127                              role_assign($sysroleid, $user->id, SYSCONTEXTID);
1128                              $this->upt->track('enrolments', get_string('assignedsysrole',
1129                                  'tool_uploaduser', $this->sysrolecache[$sysroleid]->name), 'info');
1130                          }
1131                      }
1132                  }
1133  
1134                  continue;
1135              }
1136  
1137              if (preg_match('/^categoryrole(?<roleid>\d+)$/', $column, $rolematches)) {
1138                  $categoryrolecache = [];
1139                  $categorycache  = []; // Category cache - do not fetch all categories here, we will not probably use them all.
1140  
1141                  $categoryfield = "category{$rolematches['roleid']}";
1142                  $categoryrolefield = "categoryrole{$rolematches['roleid']}";
1143  
1144                  if (empty($user->{$categoryfield})) {
1145                      continue;
1146                  }
1147  
1148                  $categoryidnumber = $user->{$categoryfield};
1149  
1150                  if (!array_key_exists($categoryidnumber, $categorycache)) {
1151                      $category = $DB->get_record('course_categories', ['idnumber' => $categoryidnumber], 'id, idnumber');
1152                      if (empty($category)) {
1153                          $this->upt->track('enrolments', get_string('unknowncategory', 'error', s($categoryidnumber)), 'error');
1154                          continue;
1155                      }
1156                      $categoryrolecache[$categoryidnumber] = uu_allowed_roles_cache($category->id);
1157                      $categoryobj = core_course_category::get($category->id);
1158                      $context = context_coursecat::instance($categoryobj->id);
1159                      $categorycache[$categoryidnumber] = $context;
1160                  }
1161                  // Check the user's category role.
1162                  if (!empty($user->{$categoryrolefield})) {
1163                      $rolename = $user->{$categoryrolefield};
1164                      if (array_key_exists($rolename, $categoryrolecache[$categoryidnumber])) {
1165                          $roleid = $categoryrolecache[$categoryidnumber][$rolename]->id;
1166                          // Assign a role to user with category context.
1167                          role_assign($roleid, $user->id, $categorycache[$categoryidnumber]->id);
1168                      } else {
1169                          $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error');
1170                          continue;
1171                      }
1172                  } else {
1173                      $this->upt->track('enrolments', get_string('missingcategoryrole', 'error', s($categoryidnumber)), 'error');
1174                      continue;
1175                  }
1176              }
1177  
1178              if (!preg_match('/^course\d+$/', $column)) {
1179                  continue;
1180              }
1181              $i = substr($column, 6);
1182  
1183              if (empty($user->{'course'.$i})) {
1184                  continue;
1185              }
1186              $shortname = $user->{'course'.$i};
1187              if (!array_key_exists($shortname, $this->ccache)) {
1188                  if (!$course = $DB->get_record('course', ['shortname' => $shortname], 'id, shortname')) {
1189                      $this->upt->track('enrolments', get_string('unknowncourse', 'error', s($shortname)), 'error');
1190                      continue;
1191                  }
1192                  $this->ccache[$shortname] = $course;
1193                  $this->ccache[$shortname]->groups = null;
1194              }
1195              $courseid      = $this->ccache[$shortname]->id;
1196              $coursecontext = \context_course::instance($courseid);
1197              if (!isset($this->manualcache[$courseid])) {
1198                  $this->manualcache[$courseid] = false;
1199                  if ($this->manualenrol) {
1200                      if ($instances = enrol_get_instances($courseid, false)) {
1201                          foreach ($instances as $instance) {
1202                              if ($instance->enrol === 'manual') {
1203                                  $this->manualcache[$courseid] = $instance;
1204                                  break;
1205                              }
1206                          }
1207                      }
1208                  }
1209              }
1210  
1211              if (!array_key_exists($courseid, $this->rolecache)) {
1212                  $this->rolecache[$courseid] = uu_allowed_roles_cache(null, (int)$courseid);
1213              }
1214  
1215              if ($courseid == SITEID) {
1216                  // Technically frontpage does not have enrolments, but only role assignments,
1217                  // let's not invent new lang strings here for this rarely used feature.
1218  
1219                  if (!empty($user->{'role'.$i})) {
1220                      $rolename = $user->{'role'.$i};
1221                      if (array_key_exists($rolename, $this->rolecache[$courseid]) ) {
1222                          $roleid = $this->rolecache[$courseid][$rolename]->id;
1223                      } else {
1224                          $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error');
1225                          continue;
1226                      }
1227  
1228                      role_assign($roleid, $user->id, \context_course::instance($courseid));
1229  
1230                      $a = new \stdClass();
1231                      $a->course = $shortname;
1232                      $a->role = $this->rolecache[$courseid][$roleid]->name;
1233                      $this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info');
1234                  }
1235  
1236              } else if ($this->manualenrol and $this->manualcache[$courseid]) {
1237  
1238                  // Find role.
1239                  $roleid = false;
1240                  if (!empty($user->{'role'.$i})) {
1241                      $rolename = $user->{'role'.$i};
1242                      if (array_key_exists($rolename, $this->rolecache[$courseid])) {
1243                          $roleid = $this->rolecache[$courseid][$rolename]->id;
1244                      } else {
1245                          $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error');
1246                          continue;
1247                      }
1248  
1249                  } else if (!empty($user->{'type'.$i})) {
1250                      // If no role, then find "old" enrolment type.
1251                      $addtype = $user->{'type'.$i};
1252                      if ($addtype < 1 or $addtype > 3) {
1253                          $this->upt->track('enrolments', get_string('error').': typeN = 1|2|3', 'error');
1254                          continue;
1255                      } else if (empty($this->formdata->{'uulegacy'.$addtype})) {
1256                          continue;
1257                      } else {
1258                          $roleid = $this->formdata->{'uulegacy'.$addtype};
1259                      }
1260                  } else {
1261                      // No role specified, use the default from manual enrol plugin.
1262                      $defaultenrolroleid = (int)$this->manualcache[$courseid]->roleid;
1263                      // Validate the current user can assign this role.
1264                      if (array_key_exists($defaultenrolroleid, $this->rolecache[$courseid]) ) {
1265                          $roleid = $defaultenrolroleid;
1266                      } else {
1267                          $role = $DB->get_record('role', ['id' => $defaultenrolroleid]);
1268                          $this->upt->track('enrolments', get_string('unknownrole', 'error', s($role->shortname)), 'error');
1269                          continue;
1270                      }
1271                  }
1272  
1273                  if ($roleid) {
1274                      // Find duration and/or enrol status.
1275                      $timeend = 0;
1276                      $timestart = $this->today;
1277                      $status = null;
1278  
1279                      if (isset($user->{'enrolstatus'.$i})) {
1280                          $enrolstatus = $user->{'enrolstatus'.$i};
1281                          if ($enrolstatus == '') {
1282                              $status = null;
1283                          } else if ($enrolstatus === (string)ENROL_USER_ACTIVE) {
1284                              $status = ENROL_USER_ACTIVE;
1285                          } else if ($enrolstatus === (string)ENROL_USER_SUSPENDED) {
1286                              $status = ENROL_USER_SUSPENDED;
1287                          } else {
1288                              debugging('Unknown enrolment status.');
1289                          }
1290                      }
1291  
1292                      if (!empty($user->{'enroltimestart'.$i})) {
1293                          $parsedtimestart = strtotime($user->{'enroltimestart'.$i});
1294                          if ($parsedtimestart !== false) {
1295                              $timestart = $parsedtimestart;
1296                          }
1297                      }
1298  
1299                      if (!empty($user->{'enrolperiod'.$i})) {
1300                          $duration = (int)$user->{'enrolperiod'.$i} * 60 * 60 * 24; // Convert days to seconds.
1301                          if ($duration > 0) { // Sanity check.
1302                              $timeend = $timestart + $duration;
1303                          }
1304                      } else if ($this->manualcache[$courseid]->enrolperiod > 0) {
1305                          $timeend = $timestart + $this->manualcache[$courseid]->enrolperiod;
1306                      }
1307  
1308                      $this->manualenrol->enrol_user($this->manualcache[$courseid], $user->id, $roleid,
1309                          $timestart, $timeend, $status);
1310  
1311                      $a = new \stdClass();
1312                      $a->course = $shortname;
1313                      $a->role = $this->rolecache[$courseid][$roleid]->name;
1314                      $this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info');
1315                  }
1316              }
1317  
1318              // Find group to add to.
1319              if (!empty($user->{'group'.$i})) {
1320                  // Make sure user is enrolled into course before adding into groups.
1321                  if (!is_enrolled($coursecontext, $user->id)) {
1322                      $this->upt->track('enrolments', get_string('addedtogroupnotenrolled', '', $user->{'group'.$i}), 'error');
1323                      continue;
1324                  }
1325                  // Build group cache.
1326                  if (is_null($this->ccache[$shortname]->groups)) {
1327                      $this->ccache[$shortname]->groups = array();
1328                      if ($groups = groups_get_all_groups($courseid)) {
1329                          foreach ($groups as $gid => $group) {
1330                              $this->ccache[$shortname]->groups[$gid] = new \stdClass();
1331                              $this->ccache[$shortname]->groups[$gid]->id   = $gid;
1332                              $this->ccache[$shortname]->groups[$gid]->name = $group->name;
1333                              if (!is_numeric($group->name)) { // Only non-numeric names are supported!!!
1334                                  $this->ccache[$shortname]->groups[$group->name] = new \stdClass();
1335                                  $this->ccache[$shortname]->groups[$group->name]->id   = $gid;
1336                                  $this->ccache[$shortname]->groups[$group->name]->name = $group->name;
1337                              }
1338                          }
1339                      }
1340                  }
1341                  // Group exists?
1342                  $addgroup = $user->{'group'.$i};
1343                  if (!array_key_exists($addgroup, $this->ccache[$shortname]->groups)) {
1344                      // If group doesn't exist,  create it.
1345                      $newgroupdata = new \stdClass();
1346                      $newgroupdata->name = $addgroup;
1347                      $newgroupdata->courseid = $this->ccache[$shortname]->id;
1348                      $newgroupdata->description = '';
1349                      $gid = groups_create_group($newgroupdata);
1350                      if ($gid) {
1351                          $this->ccache[$shortname]->groups[$addgroup] = new \stdClass();
1352                          $this->ccache[$shortname]->groups[$addgroup]->id   = $gid;
1353                          $this->ccache[$shortname]->groups[$addgroup]->name = $newgroupdata->name;
1354                      } else {
1355                          $this->upt->track('enrolments', get_string('unknowngroup', 'error', s($addgroup)), 'error');
1356                          continue;
1357                      }
1358                  }
1359                  $gid   = $this->ccache[$shortname]->groups[$addgroup]->id;
1360                  $gname = $this->ccache[$shortname]->groups[$addgroup]->name;
1361  
1362                  try {
1363                      if (groups_add_member($gid, $user->id)) {
1364                          $this->upt->track('enrolments', get_string('addedtogroup', '', s($gname)), 'info');
1365                      } else {
1366                          $this->upt->track('enrolments', get_string('addedtogroupnot', '', s($gname)), 'error');
1367                      }
1368                  } catch (\moodle_exception $e) {
1369                      $this->upt->track('enrolments', get_string('addedtogroupnot', '', s($gname)), 'error');
1370                      continue;
1371                  }
1372              }
1373          }
1374          if (($invalid = \core_user::validate($user)) !== true) {
1375              $this->upt->track('status', get_string('invaliduserdata', 'tool_uploaduser', s($user->username)), 'warning');
1376          }
1377      }
1378  
1379      /**
1380       * Summary about the whole process (how many users created, skipped, updated, etc)
1381       *
1382       * @return array
1383       */
1384      public function get_stats() {
1385          $lines = [];
1386  
1387          if ($this->get_operation_type() != UU_USER_UPDATE) {
1388              $lines[] = get_string('userscreated', 'tool_uploaduser').': '.$this->usersnew;
1389          }
1390          if ($this->get_operation_type() == UU_USER_UPDATE or $this->get_operation_type() == UU_USER_ADD_UPDATE) {
1391              $lines[] = get_string('usersupdated', 'tool_uploaduser').': '.$this->usersupdated;
1392          }
1393          if ($this->get_allow_deletes()) {
1394              $lines[] = get_string('usersdeleted', 'tool_uploaduser').': '.$this->deletes;
1395              $lines[] = get_string('deleteerrors', 'tool_uploaduser').': '.$this->deleteerrors;
1396          }
1397          if ($this->get_allow_renames()) {
1398              $lines[] = get_string('usersrenamed', 'tool_uploaduser').': '.$this->renames;
1399              $lines[] = get_string('renameerrors', 'tool_uploaduser').': '.$this->renameerrors;
1400          }
1401          if ($usersskipped = $this->usersskipped) {
1402              $lines[] = get_string('usersskipped', 'tool_uploaduser').': '.$usersskipped;
1403          }
1404          $lines[] = get_string('usersweakpassword', 'tool_uploaduser').': '.$this->weakpasswords;
1405          $lines[] = get_string('errors', 'tool_uploaduser').': '.$this->userserrors;
1406  
1407          return $lines;
1408      }
1409  }