Search moodle.org's
Developer Documentation

See Release Notes

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

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

   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 \profile_field_base[] */
  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', 'phone1', 'phone2', 'address',
 147              'description', 'descriptionformat', 'password',
 148              'auth',        // Watch out when changing auth type or using external auth plugins!
 149              'oldusername', // Use when renaming users - this is the original username.
 150              'suspended',   // 1 means suspend user account, 0 means activate user account, nothing means keep as is.
 151              'theme',       // Define a theme for user when 'allowuserthemes' is enabled.
 152              'deleted',     // 1 means delete user
 153              'mnethostid',  // Can not be used for adding, updating or deleting of users - only for enrolments,
 154                             // groups, cohorts and suspending.
 155              'interests',
 156          );
 157          // Include all name fields.
 158          $this->standardfields = array_merge($this->standardfields, \core_user\fields::get_name_fields());
 159      }
 160  
 161      /**
 162       * Profile fields
 163       */
 164      protected function find_profile_fields(): void {
 165          global $CFG;
 166          require_once($CFG->dirroot . '/user/profile/lib.php');
 167          $this->allprofilefields = profile_get_user_fields_with_data(0);
 168          $this->profilefields = [];
 169          if ($proffields = $this->allprofilefields) {
 170              foreach ($proffields as $key => $proffield) {
 171                  $profilefieldname = 'profile_field_'.$proffield->get_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              foreach ($this->allprofilefields as $field => $profilefield) {
 534                  if (isset($user->$field)) {
 535                      continue;
 536                  }
 537                  if (isset($this->formdata->$field)) {
 538                      // Process templates.
 539                      $user->$field = uu_process_template($this->formdata->$field, $user);
 540  
 541                      // Form contains key and later code expects value.
 542                      // Convert key to value for required profile fields.
 543                      if (method_exists($profilefield, 'convert_external_data')) {
 544                          $user->$field = $profilefield->edit_save_data_preprocess($user->$field, null);
 545                      }
 546  
 547                      $formdefaults[$field] = true;
 548                  }
 549              }
 550          }
 551  
 552          // Delete user.
 553          if (!empty($user->deleted)) {
 554              if (!$this->get_allow_deletes() or $remoteuser or
 555                      !has_capability('moodle/user:delete', context_system::instance())) {
 556                  $this->usersskipped++;
 557                  $this->upt->track('status', get_string('usernotdeletedoff', 'error'), 'warning');
 558                  return;
 559              }
 560              if ($existinguser) {
 561                  if (is_siteadmin($existinguser->id)) {
 562                      $this->upt->track('status', get_string('usernotdeletedadmin', 'error'), 'error');
 563                      $this->deleteerrors++;
 564                      return;
 565                  }
 566                  if (delete_user($existinguser)) {
 567                      $this->upt->track('status', get_string('userdeleted', 'tool_uploaduser'));
 568                      $this->deletes++;
 569                  } else {
 570                      $this->upt->track('status', get_string('usernotdeletederror', 'error'), 'error');
 571                      $this->deleteerrors++;
 572                  }
 573              } else {
 574                  $this->upt->track('status', get_string('usernotdeletedmissing', 'error'), 'error');
 575                  $this->deleteerrors++;
 576              }
 577              return;
 578          }
 579          // We do not need the deleted flag anymore.
 580          unset($user->deleted);
 581  
 582          // Renaming requested?
 583          if (!empty($user->oldusername) ) {
 584              if (!$this->get_allow_renames()) {
 585                  $this->usersskipped++;
 586                  $this->upt->track('status', get_string('usernotrenamedoff', 'error'), 'warning');
 587                  return;
 588              }
 589  
 590              if ($existinguser) {
 591                  $this->upt->track('status', get_string('usernotrenamedexists', 'error'), 'error');
 592                  $this->renameerrors++;
 593                  return;
 594              }
 595  
 596              if ($user->username === 'guest') {
 597                  $this->upt->track('status', get_string('guestnoeditprofileother', 'error'), 'error');
 598                  $this->renameerrors++;
 599                  return;
 600              }
 601  
 602              if ($this->get_normalise_user_names()) {
 603                  $oldusername = \core_user::clean_field($user->oldusername, 'username');
 604              } else {
 605                  $oldusername = $user->oldusername;
 606              }
 607  
 608              // No guessing when looking for old username, it must be exact match.
 609              if ($olduser = $DB->get_record('user',
 610                      ['username' => $oldusername, 'mnethostid' => $CFG->mnet_localhost_id])) {
 611                  $this->upt->track('id', $olduser->id, 'normal', false);
 612                  if (is_siteadmin($olduser->id)) {
 613                      $this->upt->track('status', get_string('usernotrenamedadmin', 'error'), 'error');
 614                      $this->renameerrors++;
 615                      return;
 616                  }
 617                  $DB->set_field('user', 'username', $user->username, ['id' => $olduser->id]);
 618                  $this->upt->track('username', '', 'normal', false); // Clear previous.
 619                  $this->upt->track('username', s($oldusername).'-->'.s($user->username), 'info');
 620                  $this->upt->track('status', get_string('userrenamed', 'tool_uploaduser'));
 621                  $this->renames++;
 622              } else {
 623                  $this->upt->track('status', get_string('usernotrenamedmissing', 'error'), 'error');
 624                  $this->renameerrors++;
 625                  return;
 626              }
 627              $existinguser = $olduser;
 628              $existinguser->username = $user->username;
 629          }
 630  
 631          // Can we process with update or insert?
 632          $skip = false;
 633          switch ($this->get_operation_type()) {
 634              case UU_USER_ADDNEW:
 635                  if ($existinguser) {
 636                      $this->usersskipped++;
 637                      $this->upt->track('status', get_string('usernotaddedregistered', 'error'), 'warning');
 638                      $skip = true;
 639                  }
 640                  break;
 641  
 642              case UU_USER_ADDINC:
 643                  if ($existinguser) {
 644                      // This should not happen!
 645                      $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error');
 646                      $this->userserrors++;
 647                      $skip = true;
 648                  }
 649                  break;
 650  
 651              case UU_USER_ADD_UPDATE:
 652                  break;
 653  
 654              case UU_USER_UPDATE:
 655                  if (!$existinguser) {
 656                      $this->usersskipped++;
 657                      $this->upt->track('status', get_string('usernotupdatednotexists', 'error'), 'warning');
 658                      $skip = true;
 659                  }
 660                  break;
 661  
 662              default:
 663                  // Unknown type.
 664                  $skip = true;
 665          }
 666  
 667          if ($skip) {
 668              return;
 669          }
 670  
 671          if ($existinguser) {
 672              $user->id = $existinguser->id;
 673  
 674              $this->upt->track('username', \html_writer::link(
 675                  new \moodle_url('/user/profile.php', ['id' => $existinguser->id]), s($existinguser->username)), 'normal', false);
 676              $this->upt->track('suspended', $this->get_string_yes_no($existinguser->suspended) , 'normal', false);
 677              $this->upt->track('auth', $existinguser->auth, 'normal', false);
 678  
 679              if (is_siteadmin($user->id)) {
 680                  $this->upt->track('status', get_string('usernotupdatedadmin', 'error'), 'error');
 681                  $this->userserrors++;
 682                  return;
 683              }
 684  
 685              $existinguser->timemodified = time();
 686              // Do NOT mess with timecreated or firstaccess here!
 687  
 688              // Load existing profile data.
 689              profile_load_data($existinguser);
 690  
 691              $doupdate = false;
 692              $dologout = false;
 693  
 694              if ($this->get_update_type() != UU_UPDATE_NOCHANGES and !$remoteuser) {
 695  
 696                  // Handle 'auth' column separately, the field can never be missing from a user.
 697                  if (!empty($user->auth) && ($user->auth !== $existinguser->auth) &&
 698                          ($this->get_update_type() != UU_UPDATE_MISSING)) {
 699  
 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  }