Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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