See Release Notes
Long Term Support Release
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class process * * @package tool_uploaduser * @copyright 2020 Moodle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace tool_uploaduser; defined('MOODLE_INTERNAL') || die(); use context_system;> use context_coursecat; use tool_uploaduser\local\field_value_validators; > use core_course_category; >require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/group/lib.php'); require_once($CFG->dirroot.'/cohort/lib.php'); require_once($CFG->libdir.'/csvlib.class.php'); require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploaduser/locallib.php'); /** * Process CSV file with users data, this will create/update users, enrol them into courses, etc * * @package tool_uploaduser * @copyright 2020 Moodle * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class process { /** @var \csv_import_reader */ protected $cir; /** @var \stdClass */ protected $formdata; /** @var \uu_progress_tracker */ protected $upt; /** @var array */ protected $filecolumns = null; /** @var int */ protected $today; /** @var \enrol_plugin|null */ protected $manualenrol = null; /** @var array */ protected $standardfields = []; /** @var array */ protected $profilefields = []; /** @var \profile_field_base[] */ protected $allprofilefields = []; /** @var string|\uu_progress_tracker|null */ protected $progresstrackerclass = null; /** @var int */ protected $usersnew = 0; /** @var int */ protected $usersupdated = 0; /** @var int /not printed yet anywhere */ protected $usersuptodate = 0; /** @var int */ protected $userserrors = 0; /** @var int */ protected $deletes = 0; /** @var int */ protected $deleteerrors = 0; /** @var int */ protected $renames = 0; /** @var int */ protected $renameerrors = 0; /** @var int */ protected $usersskipped = 0; /** @var int */ protected $weakpasswords = 0; /** @var array course cache - do not fetch all courses here, we will not probably use them all anyway */ protected $ccache = []; /** @var array */ protected $cohorts = []; /** @var array Course roles lookup cache. */ protected $rolecache = []; /** @var array System roles lookup cache. */ protected $sysrolecache = []; /** @var array cache of used manual enrol plugins in each course */ protected $manualcache = []; /** @var array officially supported plugins that are enabled */ protected $supportedauths = []; /** * process constructor. * * @param \csv_import_reader $cir * @param string|null $progresstrackerclass * @throws \coding_exception */ public function __construct(\csv_import_reader $cir, string $progresstrackerclass = null) { $this->cir = $cir; if ($progresstrackerclass) { if (!class_exists($progresstrackerclass) || !is_subclass_of($progresstrackerclass, \uu_progress_tracker::class)) { throw new \coding_exception('Progress tracker class must extend \uu_progress_tracker'); } $this->progresstrackerclass = $progresstrackerclass; } else { $this->progresstrackerclass = \uu_progress_tracker::class; } // Keep timestamp consistent. $today = time(); $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0); $this->today = $today;< $this->rolecache = uu_allowed_roles_cache(); // Course roles lookup cache.$this->sysrolecache = uu_allowed_sysroles_cache(); // System roles lookup cache. $this->supportedauths = uu_supported_auths(); // Officially supported plugins that are enabled. if (enrol_is_enabled('manual')) { // We use only manual enrol plugin here, if it is disabled no enrol is done. $this->manualenrol = enrol_get_plugin('manual'); } $this->find_profile_fields(); $this->find_standard_fields(); } /** * Standard user fields. */ protected function find_standard_fields(): void { $this->standardfields = array('id', 'username', 'email', 'emailstop', 'city', 'country', 'lang', 'timezone', 'mailformat', 'maildisplay', 'maildigest', 'htmleditor', 'autosubscribe', 'institution', 'department', 'idnumber', 'phone1', 'phone2', 'address', 'description', 'descriptionformat', 'password', 'auth', // Watch out when changing auth type or using external auth plugins! 'oldusername', // Use when renaming users - this is the original username. 'suspended', // 1 means suspend user account, 0 means activate user account, nothing means keep as is. 'theme', // Define a theme for user when 'allowuserthemes' is enabled. 'deleted', // 1 means delete user 'mnethostid', // Can not be used for adding, updating or deleting of users - only for enrolments, // groups, cohorts and suspending. 'interests', ); // Include all name fields. $this->standardfields = array_merge($this->standardfields, \core_user\fields::get_name_fields()); } /** * Profile fields */ protected function find_profile_fields(): void { global $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); $this->allprofilefields = profile_get_user_fields_with_data(0); $this->profilefields = []; if ($proffields = $this->allprofilefields) { foreach ($proffields as $key => $proffield) { $profilefieldname = 'profile_field_'.$proffield->get_shortname(); $this->profilefields[] = $profilefieldname; // Re-index $proffields with key as shortname. This will be // used while checking if profile data is key and needs to be converted (eg. menu profile field). $proffields[$profilefieldname] = $proffield; unset($proffields[$key]); } $this->allprofilefields = $proffields; } } /** * Returns the list of columns in the file * * @return array */ public function get_file_columns(): array { if ($this->filecolumns === null) { $returnurl = new \moodle_url('/admin/tool/uploaduser/index.php'); $this->filecolumns = uu_validate_user_upload_columns($this->cir, $this->standardfields, $this->profilefields, $returnurl); } return $this->filecolumns; } /** * Set data from the form (or from CLI options) * * @param \stdClass $formdata */ public function set_form_data(\stdClass $formdata): void { global $SESSION; $this->formdata = $formdata; // Clear bulk selection. if ($this->get_bulk()) { $SESSION->bulk_users = array(); } } /** * Operation type * @return int */ protected function get_operation_type(): int { return (int)$this->formdata->uutype; } /** * Setting to allow deletes * @return bool */ protected function get_allow_deletes(): bool { $optype = $this->get_operation_type(); return (!empty($this->formdata->uuallowdeletes) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC); } /**> * Setting to allow matching user accounts on email * Setting to allow deletes > * @return bool * @return bool > */ */ > protected function get_match_on_email(): bool { protected function get_allow_renames(): bool { > $optype = $this->get_operation_type(); $optype = $this->get_operation_type(); > return (!empty($this->formdata->uumatchemail) && $optype != UU_USER_ADDNEW && $optype != UU_USER_ADDINC); return (!empty($this->formdata->uuallowrenames) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC); > } } > > /**/** * Setting to select for bulk actions (not available in CLI) * @return bool */ public function get_bulk(): bool { return $this->formdata->uubulk ?? false; } /** * Setting for update type * @return int */ protected function get_update_type(): int { return isset($this->formdata->uuupdatetype) ? $this->formdata->uuupdatetype : 0; } /** * Setting to allow update passwords * @return bool */ protected function get_update_passwords(): bool { return !empty($this->formdata->uupasswordold) and $this->get_operation_type() != UU_USER_ADDNEW and $this->get_operation_type() != UU_USER_ADDINC and ($this->get_update_type() == UU_UPDATE_FILEOVERRIDE or $this->get_update_type() == UU_UPDATE_ALLOVERRIDE); } /** * Setting to allow email duplicates * @return bool */ protected function get_allow_email_duplicates(): bool { global $CFG; return !(empty($CFG->allowaccountssameemail) ? 1 : $this->formdata->uunoemailduplicates); } /** * Setting for reset password * @return int UU_PWRESET_NONE, UU_PWRESET_WEAK, UU_PWRESET_ALL */ protected function get_reset_passwords(): int { return isset($this->formdata->uuforcepasswordchange) ? $this->formdata->uuforcepasswordchange : UU_PWRESET_NONE; } /** * Setting to allow create passwords * @return bool */ protected function get_create_paswords(): bool { return (!empty($this->formdata->uupasswordnew) and $this->get_operation_type() != UU_USER_UPDATE); } /** * Setting to allow suspends * @return bool */ protected function get_allow_suspends(): bool { return !empty($this->formdata->uuallowsuspends); } /** * Setting to normalise user names * @return bool */ protected function get_normalise_user_names(): bool { return !empty($this->formdata->uustandardusernames); } /** * Helper method to return Yes/No string * * @param bool $value * @return string */ protected function get_string_yes_no($value): string { return $value ? get_string('yes') : get_string('no'); } /** * Process the CSV file */ public function process() { // Init csv import helper. $this->cir->init(); $classname = $this->progresstrackerclass; $this->upt = new $classname(); $this->upt->start(); // Start table. $linenum = 1; // Column header is first line. while ($line = $this->cir->next()) { $this->upt->flush(); $linenum++; $this->upt->track('line', $linenum); $this->process_line($line); } $this->upt->close(); // Close table. $this->cir->close(); $this->cir->cleanup(true); } /** * Prepare one line from CSV file as a user record * * @param array $line * @return \stdClass|null */ protected function prepare_user_record(array $line): ?\stdClass { global $CFG, $USER; $user = new \stdClass(); // Add fields to user object. foreach ($line as $keynum => $value) { if (!isset($this->get_file_columns()[$keynum])) { // This should not happen. continue; } $key = $this->get_file_columns()[$keynum]; if (strpos($key, 'profile_field_') === 0) { // NOTE: bloody mega hack alert!! if (isset($USER->$key) and is_array($USER->$key)) { // This must be some hacky field that is abusing arrays to store content and format. $user->$key = array(); $user->{$key['text']} = $value; $user->{$key['format']} = FORMAT_MOODLE; } else { $user->$key = trim($value); } } else { $user->$key = trim($value); } if (in_array($key, $this->upt->columns)) { // Default value in progress tracking table, can be changed later. $this->upt->track($key, s($value), 'normal'); } } if (!isset($user->username)) { // Prevent warnings below. $user->username = ''; } if ($this->get_operation_type() == UU_USER_ADDNEW or $this->get_operation_type() == UU_USER_ADDINC) { // User creation is a special case - the username may be constructed from templates using firstname and lastname // better never try this in mixed update types. $error = false; if (!isset($user->firstname) or $user->firstname === '') { $this->upt->track('status', get_string('missingfield', 'error', 'firstname'), 'error'); $this->upt->track('firstname', get_string('error'), 'error'); $error = true; } if (!isset($user->lastname) or $user->lastname === '') { $this->upt->track('status', get_string('missingfield', 'error', 'lastname'), 'error'); $this->upt->track('lastname', get_string('error'), 'error'); $error = true; } if ($error) { $this->userserrors++; return null; } // We require username too - we might use template for it though. if (empty($user->username) and !empty($this->formdata->username)) { $user->username = uu_process_template($this->formdata->username, $user); $this->upt->track('username', s($user->username)); } } // Normalize username. $user->originalusername = $user->username; if ($this->get_normalise_user_names()) { $user->username = \core_user::clean_field($user->username, 'username'); } // Make sure we really have username.< if (empty($user->username)) {> if (empty($user->username) && !$this->get_match_on_email()) {$this->upt->track('status', get_string('missingfield', 'error', 'username'), 'error'); $this->upt->track('username', get_string('error'), 'error'); $this->userserrors++; return null; } else if ($user->username === 'guest') { $this->upt->track('status', get_string('guestnoeditprofileother', 'error'), 'error'); $this->userserrors++; return null; } if ($user->username !== \core_user::clean_field($user->username, 'username')) { $this->upt->track('status', get_string('invalidusername', 'error', 'username'), 'error'); $this->upt->track('username', get_string('error'), 'error'); $this->userserrors++; } if (empty($user->mnethostid)) { $user->mnethostid = $CFG->mnet_localhost_id; } return $user; } /** * Process one line from CSV file * * @param array $line * @throws \coding_exception * @throws \dml_exception * @throws \moodle_exception */ public function process_line(array $line) { global $DB, $CFG, $SESSION; if (!$user = $this->prepare_user_record($line)) { return; }< if ($existinguser = $DB->get_record('user', ['username' => $user->username, 'mnethostid' => $user->mnethostid])) {> $matchparam = $this->get_match_on_email() ? ['email' => $user->email] : ['username' => $user->username]; > if ($existinguser = $DB->get_records('user', $matchparam + ['mnethostid' => $user->mnethostid])) { > if (is_array($existinguser) && count($existinguser) !== 1) { > $this->upt->track('status', get_string('duplicateemail', 'tool_uploaduser', $user->email), 'warning'); > $this->userserrors++; > return; > > } > > $existinguser = is_array($existinguser) ? array_values($existinguser)[0] : $existinguser;$this->upt->track('id', $existinguser->id, 'normal', false); } if ($user->mnethostid == $CFG->mnet_localhost_id) { $remoteuser = false; // Find out if username incrementing required. if ($existinguser and $this->get_operation_type() == UU_USER_ADDINC) { $user->username = uu_increment_username($user->username); $existinguser = false; } } else { if (!$existinguser or $this->get_operation_type() == UU_USER_ADDINC) { $this->upt->track('status', get_string('errormnetadd', 'tool_uploaduser'), 'error'); $this->userserrors++; return; } $remoteuser = true; // Make sure there are no changes of existing fields except the suspended status. foreach ((array)$existinguser as $k => $v) { if ($k === 'suspended') { continue; } if (property_exists($user, $k)) { $user->$k = $v; } if (in_array($k, $this->upt->columns)) { if ($k === 'password' or $k === 'oldusername' or $k === 'deleted') { $this->upt->track($k, '', 'normal', false); } else { $this->upt->track($k, s($v), 'normal', false); } } } unset($user->oldusername); unset($user->password); $user->auth = $existinguser->auth; } // Notify about nay username changes. if ($user->originalusername !== $user->username) { $this->upt->track('username', '', 'normal', false); // Clear previous. $this->upt->track('username', s($user->originalusername).'-->'.s($user->username), 'info'); } else { $this->upt->track('username', s($user->username), 'normal', false); } unset($user->originalusername); // Verify if the theme is valid and allowed to be set. if (isset($user->theme)) { list($status, $message) = field_value_validators::validate_theme($user->theme); if ($status !== 'normal' && !empty($message)) { $this->upt->track('status', $message, $status); // Unset the theme when validation fails. unset($user->theme); } } // Add default values for remaining fields. $formdefaults = array(); if (!$existinguser || ($this->get_update_type() != UU_UPDATE_FILEOVERRIDE && $this->get_update_type() != UU_UPDATE_NOCHANGES)) { foreach ($this->standardfields as $field) { if (isset($user->$field)) { continue; } // All validation moved to form2. if (isset($this->formdata->$field)) { // Process templates. $user->$field = uu_process_template($this->formdata->$field, $user); $formdefaults[$field] = true; if (in_array($field, $this->upt->columns)) { $this->upt->track($field, s($user->$field), 'normal'); } } } foreach ($this->allprofilefields as $field => $profilefield) { if (isset($user->$field)) { continue; } if (isset($this->formdata->$field)) { // Process templates. $user->$field = uu_process_template($this->formdata->$field, $user); // Form contains key and later code expects value. // Convert key to value for required profile fields. if (method_exists($profilefield, 'convert_external_data')) { $user->$field = $profilefield->edit_save_data_preprocess($user->$field, null); } $formdefaults[$field] = true; } } } // Delete user. if (!empty($user->deleted)) { if (!$this->get_allow_deletes() or $remoteuser or !has_capability('moodle/user:delete', context_system::instance())) { $this->usersskipped++; $this->upt->track('status', get_string('usernotdeletedoff', 'error'), 'warning'); return; } if ($existinguser) { if (is_siteadmin($existinguser->id)) { $this->upt->track('status', get_string('usernotdeletedadmin', 'error'), 'error'); $this->deleteerrors++; return; } if (delete_user($existinguser)) { $this->upt->track('status', get_string('userdeleted', 'tool_uploaduser')); $this->deletes++; } else { $this->upt->track('status', get_string('usernotdeletederror', 'error'), 'error'); $this->deleteerrors++; } } else { $this->upt->track('status', get_string('usernotdeletedmissing', 'error'), 'error'); $this->deleteerrors++; } return; } // We do not need the deleted flag anymore. unset($user->deleted);> $matchonemailallowrename = $this->get_match_on_email() && $this->get_allow_renames(); // Renaming requested? > if ($matchonemailallowrename && $user->username && ($user->username !== $existinguser->username)) { if (!empty($user->oldusername) ) { > $user->oldusername = $existinguser->username; if (!$this->get_allow_renames()) { > $existinguser = false; $this->usersskipped++; > } $this->upt->track('status', get_string('usernotrenamedoff', 'error'), 'warning'); >return; } if ($existinguser) { $this->upt->track('status', get_string('usernotrenamedexists', 'error'), 'error'); $this->renameerrors++; return; } if ($user->username === 'guest') { $this->upt->track('status', get_string('guestnoeditprofileother', 'error'), 'error'); $this->renameerrors++; return; } if ($this->get_normalise_user_names()) { $oldusername = \core_user::clean_field($user->oldusername, 'username'); } else { $oldusername = $user->oldusername; } // No guessing when looking for old username, it must be exact match. if ($olduser = $DB->get_record('user', ['username' => $oldusername, 'mnethostid' => $CFG->mnet_localhost_id])) { $this->upt->track('id', $olduser->id, 'normal', false); if (is_siteadmin($olduser->id)) { $this->upt->track('status', get_string('usernotrenamedadmin', 'error'), 'error'); $this->renameerrors++; return; } $DB->set_field('user', 'username', $user->username, ['id' => $olduser->id]); $this->upt->track('username', '', 'normal', false); // Clear previous. $this->upt->track('username', s($oldusername).'-->'.s($user->username), 'info'); $this->upt->track('status', get_string('userrenamed', 'tool_uploaduser')); $this->renames++; } else { $this->upt->track('status', get_string('usernotrenamedmissing', 'error'), 'error'); $this->renameerrors++; return; } $existinguser = $olduser; $existinguser->username = $user->username; } // Can we process with update or insert? $skip = false; switch ($this->get_operation_type()) { case UU_USER_ADDNEW: if ($existinguser) { $this->usersskipped++; $this->upt->track('status', get_string('usernotaddedregistered', 'error'), 'warning'); $skip = true; } break; case UU_USER_ADDINC: if ($existinguser) { // This should not happen! $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); $this->userserrors++; $skip = true; } break; case UU_USER_ADD_UPDATE: break; case UU_USER_UPDATE: if (!$existinguser) { $this->usersskipped++; $this->upt->track('status', get_string('usernotupdatednotexists', 'error'), 'warning'); $skip = true; } break; default: // Unknown type. $skip = true; } if ($skip) { return; } if ($existinguser) { $user->id = $existinguser->id; $this->upt->track('username', \html_writer::link( new \moodle_url('/user/profile.php', ['id' => $existinguser->id]), s($existinguser->username)), 'normal', false); $this->upt->track('suspended', $this->get_string_yes_no($existinguser->suspended) , 'normal', false); $this->upt->track('auth', $existinguser->auth, 'normal', false); if (is_siteadmin($user->id)) { $this->upt->track('status', get_string('usernotupdatedadmin', 'error'), 'error'); $this->userserrors++; return; } $existinguser->timemodified = time(); // Do NOT mess with timecreated or firstaccess here! // Load existing profile data. profile_load_data($existinguser); $doupdate = false; $dologout = false; if ($this->get_update_type() != UU_UPDATE_NOCHANGES and !$remoteuser) { // Handle 'auth' column separately, the field can never be missing from a user. if (!empty($user->auth) && ($user->auth !== $existinguser->auth) && ($this->get_update_type() != UU_UPDATE_MISSING)) { $this->upt->track('auth', s($existinguser->auth).'-->'.s($user->auth), 'info', false); $existinguser->auth = $user->auth; if (!isset($this->supportedauths[$user->auth])) { $this->upt->track('auth', get_string('userauthunsupported', 'error'), 'warning'); } $doupdate = true; if ($existinguser->auth === 'nologin') { $dologout = true; } } $allcolumns = array_merge($this->standardfields, $this->profilefields); foreach ($allcolumns as $column) { if ($column === 'username' or $column === 'password' or $column === 'auth' or $column === 'suspended') { // These can not be changed here. continue; } if (!property_exists($user, $column) or !property_exists($existinguser, $column)) { continue; } if ($this->get_update_type() == UU_UPDATE_MISSING) { if (!is_null($existinguser->$column) and $existinguser->$column !== '') { continue; } } else if ($this->get_update_type() == UU_UPDATE_ALLOVERRIDE) { // We override everything. null; } else if ($this->get_update_type() == UU_UPDATE_FILEOVERRIDE) { if (!empty($formdefaults[$column])) { // Do not override with form defaults. continue; } } if ($existinguser->$column !== $user->$column) { if ($column === 'email') { $select = $DB->sql_like('email', ':email', false, true, false, '|'); $params = array('email' => $DB->sql_like_escape($user->email, '|')); if ($DB->record_exists_select('user', $select , $params)) { $changeincase = \core_text::strtolower($existinguser->$column) === \core_text::strtolower( $user->$column); if ($changeincase) { // If only case is different then switch to lower case and carry on. $user->$column = \core_text::strtolower($user->$column); continue; } else if (!$this->get_allow_email_duplicates()) { $this->upt->track('email', get_string('useremailduplicate', 'error'), 'error'); $this->upt->track('status', get_string('usernotupdatederror', 'error'), 'error'); $this->userserrors++; return; } else { $this->upt->track('email', get_string('useremailduplicate', 'error'), 'warning'); } } if (!validate_email($user->email)) { $this->upt->track('email', get_string('invalidemail'), 'warning'); } } if ($column === 'lang') { if (empty($user->lang)) { // Do not change to not-set value. continue; } else if (\core_user::clean_field($user->lang, 'lang') === '') { $this->upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning'); continue; } } if (in_array($column, $this->upt->columns)) { $this->upt->track($column, s($existinguser->$column).'-->'.s($user->$column), 'info', false); } $existinguser->$column = $user->$column; $doupdate = true; } } } try { $auth = get_auth_plugin($existinguser->auth); } catch (\Exception $e) { $this->upt->track('auth', get_string('userautherror', 'error', s($existinguser->auth)), 'error'); $this->upt->track('status', get_string('usernotupdatederror', 'error'), 'error'); $this->userserrors++; return; } $isinternalauth = $auth->is_internal(); // Deal with suspending and activating of accounts. if ($this->get_allow_suspends() and isset($user->suspended) and $user->suspended !== '') { $user->suspended = $user->suspended ? 1 : 0; if ($existinguser->suspended != $user->suspended) { $this->upt->track('suspended', '', 'normal', false); $this->upt->track('suspended', $this->get_string_yes_no($existinguser->suspended).'-->'.$this->get_string_yes_no($user->suspended), 'info', false); $existinguser->suspended = $user->suspended; $doupdate = true; if ($existinguser->suspended) { $dologout = true; } } } // Changing of passwords is a special case // do not force password changes for external auth plugins! $oldpw = $existinguser->password; if ($remoteuser) { // Do not mess with passwords of remote users. null; } else if (!$isinternalauth) { $existinguser->password = AUTH_PASSWORD_NOT_CACHED; $this->upt->track('password', '-', 'normal', false); // Clean up prefs. unset_user_preference('create_password', $existinguser); unset_user_preference('auth_forcepasswordchange', $existinguser); } else if (!empty($user->password)) { if ($this->get_update_passwords()) { // Check for passwords that we want to force users to reset next // time they log in. $errmsg = null; $weak = !check_password_policy($user->password, $errmsg, $user); if ($this->get_reset_passwords() == UU_PWRESET_ALL or ($this->get_reset_passwords() == UU_PWRESET_WEAK and $weak)) { if ($weak) { $this->weakpasswords++; $this->upt->track('password', get_string('invalidpasswordpolicy', 'error'), 'warning'); } set_user_preference('auth_forcepasswordchange', 1, $existinguser); } else { unset_user_preference('auth_forcepasswordchange', $existinguser); } unset_user_preference('create_password', $existinguser); // No need to create password any more. // Use a low cost factor when generating bcrypt hash otherwise // hashing would be slow when uploading lots of users. Hashes // will be automatically updated to a higher cost factor the first // time the user logs in. $existinguser->password = hash_internal_user_password($user->password, true); $this->upt->track('password', $user->password, 'normal', false); } else { // Do not print password when not changed. $this->upt->track('password', '', 'normal', false); } } if ($doupdate or $existinguser->password !== $oldpw) { // We want only users that were really updated. user_update_user($existinguser, false, false); $this->upt->track('status', get_string('useraccountupdated', 'tool_uploaduser')); $this->usersupdated++; if (!$remoteuser) { // Pre-process custom profile menu fields data from csv file. $existinguser = uu_pre_process_custom_profile_data($existinguser); // Save custom profile fields data from csv file. profile_save_data($existinguser); } if ($this->get_bulk() == UU_BULK_UPDATED or $this->get_bulk() == UU_BULK_ALL) { if (!in_array($user->id, $SESSION->bulk_users)) { $SESSION->bulk_users[] = $user->id; } } // Trigger event. \core\event\user_updated::create_from_userid($existinguser->id)->trigger(); } else { // No user information changed. $this->upt->track('status', get_string('useraccountuptodate', 'tool_uploaduser')); $this->usersuptodate++; if ($this->get_bulk() == UU_BULK_ALL) { if (!in_array($user->id, $SESSION->bulk_users)) { $SESSION->bulk_users[] = $user->id; } } } if ($dologout) { \core\session\manager::kill_user_sessions($existinguser->id); } } else { // Save the new user to the database. $user->confirmed = 1; $user->timemodified = time(); $user->timecreated = time(); $user->mnethostid = $CFG->mnet_localhost_id; // We support ONLY local accounts here, sorry. if (!isset($user->suspended) or $user->suspended === '') { $user->suspended = 0; } else { $user->suspended = $user->suspended ? 1 : 0; } $this->upt->track('suspended', $this->get_string_yes_no($user->suspended), 'normal', false); if (empty($user->auth)) { $user->auth = 'manual'; } $this->upt->track('auth', $user->auth, 'normal', false); // Do not insert record if new auth plugin does not exist! try { $auth = get_auth_plugin($user->auth); } catch (\Exception $e) { $this->upt->track('auth', get_string('userautherror', 'error', s($user->auth)), 'error'); $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); $this->userserrors++; return; } if (!isset($this->supportedauths[$user->auth])) { $this->upt->track('auth', get_string('userauthunsupported', 'error'), 'warning'); } $isinternalauth = $auth->is_internal(); if (empty($user->email)) { $this->upt->track('email', get_string('invalidemail'), 'error'); $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); $this->userserrors++; return; } else if ($DB->record_exists('user', ['email' => $user->email])) { if (!$this->get_allow_email_duplicates()) { $this->upt->track('email', get_string('useremailduplicate', 'error'), 'error'); $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); $this->userserrors++; return; } else { $this->upt->track('email', get_string('useremailduplicate', 'error'), 'warning'); } } if (!validate_email($user->email)) { $this->upt->track('email', get_string('invalidemail'), 'warning'); } if (empty($user->lang)) { $user->lang = ''; } else if (\core_user::clean_field($user->lang, 'lang') === '') { $this->upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning'); $user->lang = ''; } $forcechangepassword = false; if ($isinternalauth) { if (empty($user->password)) { if ($this->get_create_paswords()) { $user->password = 'to be generated'; $this->upt->track('password', '', 'normal', false); $this->upt->track('password', get_string('uupasswordcron', 'tool_uploaduser'), 'warning', false); } else { $this->upt->track('password', '', 'normal', false); $this->upt->track('password', get_string('missingfield', 'error', 'password'), 'error'); $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); $this->userserrors++; return; } } else { $errmsg = null; $weak = !check_password_policy($user->password, $errmsg, $user); if ($this->get_reset_passwords() == UU_PWRESET_ALL or ($this->get_reset_passwords() == UU_PWRESET_WEAK and $weak)) { if ($weak) { $this->weakpasswords++; $this->upt->track('password', get_string('invalidpasswordpolicy', 'error'), 'warning'); } $forcechangepassword = true; } // Use a low cost factor when generating bcrypt hash otherwise // hashing would be slow when uploading lots of users. Hashes // will be automatically updated to a higher cost factor the first // time the user logs in. $user->password = hash_internal_user_password($user->password, true); } } else { $user->password = AUTH_PASSWORD_NOT_CACHED; $this->upt->track('password', '-', 'normal', false); } $user->id = user_create_user($user, false, false); $this->upt->track('username', \html_writer::link( new \moodle_url('/user/profile.php', ['id' => $user->id]), s($user->username)), 'normal', false); // Pre-process custom profile menu fields data from csv file. $user = uu_pre_process_custom_profile_data($user); // Save custom profile fields data. profile_save_data($user); if ($forcechangepassword) { set_user_preference('auth_forcepasswordchange', 1, $user); } if ($user->password === 'to be generated') { set_user_preference('create_password', 1, $user); } // Trigger event. \core\event\user_created::create_from_userid($user->id)->trigger(); $this->upt->track('status', get_string('newuser')); $this->upt->track('id', $user->id, 'normal', false); $this->usersnew++; // Make sure user context exists. \context_user::instance($user->id); if ($this->get_bulk() == UU_BULK_NEW or $this->get_bulk() == UU_BULK_ALL) { if (!in_array($user->id, $SESSION->bulk_users)) { $SESSION->bulk_users[] = $user->id; } } } // Update user interests. if (isset($user->interests) && strval($user->interests) !== '') { useredit_update_interests($user, preg_split('/\s*,\s*/', $user->interests, -1, PREG_SPLIT_NO_EMPTY)); } // Add to cohort first, it might trigger enrolments indirectly - do NOT create cohorts here! foreach ($this->get_file_columns() as $column) { if (!preg_match('/^cohort\d+$/', $column)) { continue; } if (!empty($user->$column)) { $addcohort = $user->$column; if (!isset($this->cohorts[$addcohort])) { if (is_number($addcohort)) { // Only non-numeric idnumbers! $cohort = $DB->get_record('cohort', ['id' => $addcohort]); } else { $cohort = $DB->get_record('cohort', ['idnumber' => $addcohort]); if (empty($cohort) && has_capability('moodle/cohort:manage', \context_system::instance())) { // Cohort was not found. Create a new one. $cohortid = cohort_add_cohort((object)array( 'idnumber' => $addcohort, 'name' => $addcohort, 'contextid' => \context_system::instance()->id )); $cohort = $DB->get_record('cohort', ['id' => $cohortid]); } } if (empty($cohort)) { $this->cohorts[$addcohort] = get_string('unknowncohort', 'core_cohort', s($addcohort)); } else if (!empty($cohort->component)) { // Cohorts synchronised with external sources must not be modified! $this->cohorts[$addcohort] = get_string('external', 'core_cohort'); } else { $this->cohorts[$addcohort] = $cohort; } } if (is_object($this->cohorts[$addcohort])) { $cohort = $this->cohorts[$addcohort]; if (!$DB->record_exists('cohort_members', ['cohortid' => $cohort->id, 'userid' => $user->id])) { cohort_add_member($cohort->id, $user->id); // We might add special column later, for now let's abuse enrolments. $this->upt->track('enrolments', get_string('useradded', 'core_cohort', s($cohort->name)), 'info'); } } else { // Error message. $this->upt->track('enrolments', $this->cohorts[$addcohort], 'error'); } } } // Find course enrolments, groups, roles/types and enrol periods // this is again a special case, we always do this for any updated or created users. foreach ($this->get_file_columns() as $column) { if (preg_match('/^sysrole\d+$/', $column)) { if (!empty($user->$column)) { $sysrolename = $user->$column; if ($sysrolename[0] == '-') { $removing = true; $sysrolename = substr($sysrolename, 1); } else { $removing = false; } if (array_key_exists($sysrolename, $this->sysrolecache)) { $sysroleid = $this->sysrolecache[$sysrolename]->id; } else { $this->upt->track('enrolments', get_string('unknownrole', 'error', s($sysrolename)), 'error'); continue; } if ($removing) { if (user_has_role_assignment($user->id, $sysroleid, SYSCONTEXTID)) { role_unassign($sysroleid, $user->id, SYSCONTEXTID); $this->upt->track('enrolments', get_string('unassignedsysrole', 'tool_uploaduser', $this->sysrolecache[$sysroleid]->name), 'info'); } } else { if (!user_has_role_assignment($user->id, $sysroleid, SYSCONTEXTID)) { role_assign($sysroleid, $user->id, SYSCONTEXTID); $this->upt->track('enrolments', get_string('assignedsysrole', 'tool_uploaduser', $this->sysrolecache[$sysroleid]->name), 'info'); } } } continue; }> if (!preg_match('/^course\d+$/', $column)) { > if (preg_match('/^categoryrole(?<roleid>\d+)$/', $column, $rolematches)) { continue; > $categoryrolecache = []; } > $categorycache = []; // Category cache - do not fetch all categories here, we will not probably use them all. $i = substr($column, 6); > > $categoryfield = "category{$rolematches['roleid']}"; if (empty($user->{'course'.$i})) { > $categoryrolefield = "categoryrole{$rolematches['roleid']}"; continue; > } > if (empty($user->{$categoryfield})) { $shortname = $user->{'course'.$i}; > continue; if (!array_key_exists($shortname, $this->ccache)) { > } if (!$course = $DB->get_record('course', ['shortname' => $shortname], 'id, shortname')) { > $this->upt->track('enrolments', get_string('unknowncourse', 'error', s($shortname)), 'error'); > $categoryidnumber = $user->{$categoryfield}; continue; > } > if (!array_key_exists($categoryidnumber, $categorycache)) { $this->ccache[$shortname] = $course; > $category = $DB->get_record('course_categories', ['idnumber' => $categoryidnumber], 'id, idnumber'); $this->ccache[$shortname]->groups = null; > if (empty($category)) { } > $this->upt->track('enrolments', get_string('unknowncategory', 'error', s($categoryidnumber)), 'error'); $courseid = $this->ccache[$shortname]->id; > continue; $coursecontext = \context_course::instance($courseid); > } if (!isset($this->manualcache[$courseid])) { > $categoryrolecache[$categoryidnumber] = uu_allowed_roles_cache($category->id); $this->manualcache[$courseid] = false; > $categoryobj = core_course_category::get($category->id); if ($this->manualenrol) { > $context = context_coursecat::instance($categoryobj->id); if ($instances = enrol_get_instances($courseid, false)) { > $categorycache[$categoryidnumber] = $context; foreach ($instances as $instance) { > } if ($instance->enrol === 'manual') { > // Check the user's category role. $this->manualcache[$courseid] = $instance; > if (!empty($user->{$categoryrolefield})) { break; > $rolename = $user->{$categoryrolefield}; } > if (array_key_exists($rolename, $categoryrolecache[$categoryidnumber])) { } > $roleid = $categoryrolecache[$categoryidnumber][$rolename]->id; } > // Assign a role to user with category context. } > role_assign($roleid, $user->id, $categorycache[$categoryidnumber]->id); } > } else { > $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error'); if ($courseid == SITEID) { > continue; // Technically frontpage does not have enrolments, but only role assignments, > } // let's not invent new lang strings here for this rarely used feature. > } else { > $this->upt->track('enrolments', get_string('missingcategoryrole', 'error', s($categoryidnumber)), 'error'); if (!empty($user->{'role'.$i})) { > continue; $rolename = $user->{'role'.$i}; > } if (array_key_exists($rolename, $this->rolecache)) { > } $roleid = $this->rolecache[$rolename]->id; >} else {> if (!array_key_exists($courseid, $this->rolecache)) { $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error'); > $this->rolecache[$courseid] = uu_allowed_roles_cache(null, (int)$courseid); continue; > } } >< if (array_key_exists($rolename, $this->rolecache)) { < $roleid = $this->rolecache[$rolename]->id;> if (array_key_exists($rolename, $this->rolecache[$courseid]) ) { > $roleid = $this->rolecache[$courseid][$rolename]->id;$a = new \stdClass(); $a->course = $shortname;< $a->role = $this->rolecache[$roleid]->name;> $a->role = $this->rolecache[$courseid][$roleid]->name;$this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info'); } } else if ($this->manualenrol and $this->manualcache[$courseid]) { // Find role. $roleid = false; if (!empty($user->{'role'.$i})) { $rolename = $user->{'role'.$i};< if (array_key_exists($rolename, $this->rolecache)) { < $roleid = $this->rolecache[$rolename]->id;> if (array_key_exists($rolename, $this->rolecache[$courseid])) { > $roleid = $this->rolecache[$courseid][$rolename]->id;} else { $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error'); continue; } } else if (!empty($user->{'type'.$i})) { // If no role, then find "old" enrolment type. $addtype = $user->{'type'.$i}; if ($addtype < 1 or $addtype > 3) { $this->upt->track('enrolments', get_string('error').': typeN = 1|2|3', 'error'); continue; } else if (empty($this->formdata->{'uulegacy'.$addtype})) { continue; } else { $roleid = $this->formdata->{'uulegacy'.$addtype}; } } else { // No role specified, use the default from manual enrol plugin.< $roleid = $this->manualcache[$courseid]->roleid;> $defaultenrolroleid = (int)$this->manualcache[$courseid]->roleid; > // Validate the current user can assign this role. > if (array_key_exists($defaultenrolroleid, $this->rolecache[$courseid]) ) { > $roleid = $defaultenrolroleid; > } else { > $role = $DB->get_record('role', ['id' => $defaultenrolroleid]); > $this->upt->track('enrolments', get_string('unknownrole', 'error', s($role->shortname)), 'error'); > continue; > }} if ($roleid) { // Find duration and/or enrol status. $timeend = 0; $timestart = $this->today; $status = null; if (isset($user->{'enrolstatus'.$i})) { $enrolstatus = $user->{'enrolstatus'.$i}; if ($enrolstatus == '') { $status = null; } else if ($enrolstatus === (string)ENROL_USER_ACTIVE) { $status = ENROL_USER_ACTIVE; } else if ($enrolstatus === (string)ENROL_USER_SUSPENDED) { $status = ENROL_USER_SUSPENDED; } else { debugging('Unknown enrolment status.'); } } if (!empty($user->{'enroltimestart'.$i})) { $parsedtimestart = strtotime($user->{'enroltimestart'.$i}); if ($parsedtimestart !== false) { $timestart = $parsedtimestart; } } if (!empty($user->{'enrolperiod'.$i})) { $duration = (int)$user->{'enrolperiod'.$i} * 60 * 60 * 24; // Convert days to seconds. if ($duration > 0) { // Sanity check. $timeend = $timestart + $duration; } } else if ($this->manualcache[$courseid]->enrolperiod > 0) { $timeend = $timestart + $this->manualcache[$courseid]->enrolperiod; } $this->manualenrol->enrol_user($this->manualcache[$courseid], $user->id, $roleid, $timestart, $timeend, $status); $a = new \stdClass(); $a->course = $shortname;< $a->role = $this->rolecache[$roleid]->name;> $a->role = $this->rolecache[$courseid][$roleid]->name;$this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info'); } } // Find group to add to. if (!empty($user->{'group'.$i})) { // Make sure user is enrolled into course before adding into groups. if (!is_enrolled($coursecontext, $user->id)) { $this->upt->track('enrolments', get_string('addedtogroupnotenrolled', '', $user->{'group'.$i}), 'error'); continue; } // Build group cache. if (is_null($this->ccache[$shortname]->groups)) { $this->ccache[$shortname]->groups = array(); if ($groups = groups_get_all_groups($courseid)) { foreach ($groups as $gid => $group) { $this->ccache[$shortname]->groups[$gid] = new \stdClass(); $this->ccache[$shortname]->groups[$gid]->id = $gid; $this->ccache[$shortname]->groups[$gid]->name = $group->name; if (!is_numeric($group->name)) { // Only non-numeric names are supported!!! $this->ccache[$shortname]->groups[$group->name] = new \stdClass(); $this->ccache[$shortname]->groups[$group->name]->id = $gid; $this->ccache[$shortname]->groups[$group->name]->name = $group->name; } } } } // Group exists? $addgroup = $user->{'group'.$i}; if (!array_key_exists($addgroup, $this->ccache[$shortname]->groups)) { // If group doesn't exist, create it. $newgroupdata = new \stdClass(); $newgroupdata->name = $addgroup; $newgroupdata->courseid = $this->ccache[$shortname]->id; $newgroupdata->description = ''; $gid = groups_create_group($newgroupdata); if ($gid) { $this->ccache[$shortname]->groups[$addgroup] = new \stdClass(); $this->ccache[$shortname]->groups[$addgroup]->id = $gid; $this->ccache[$shortname]->groups[$addgroup]->name = $newgroupdata->name; } else { $this->upt->track('enrolments', get_string('unknowngroup', 'error', s($addgroup)), 'error'); continue; } } $gid = $this->ccache[$shortname]->groups[$addgroup]->id; $gname = $this->ccache[$shortname]->groups[$addgroup]->name; try { if (groups_add_member($gid, $user->id)) { $this->upt->track('enrolments', get_string('addedtogroup', '', s($gname)), 'info'); } else { $this->upt->track('enrolments', get_string('addedtogroupnot', '', s($gname)), 'error'); } } catch (\moodle_exception $e) { $this->upt->track('enrolments', get_string('addedtogroupnot', '', s($gname)), 'error'); continue; } } } if (($invalid = \core_user::validate($user)) !== true) { $this->upt->track('status', get_string('invaliduserdata', 'tool_uploaduser', s($user->username)), 'warning'); } } /** * Summary about the whole process (how many users created, skipped, updated, etc) * * @return array */ public function get_stats() { $lines = []; if ($this->get_operation_type() != UU_USER_UPDATE) { $lines[] = get_string('userscreated', 'tool_uploaduser').': '.$this->usersnew; } if ($this->get_operation_type() == UU_USER_UPDATE or $this->get_operation_type() == UU_USER_ADD_UPDATE) { $lines[] = get_string('usersupdated', 'tool_uploaduser').': '.$this->usersupdated; } if ($this->get_allow_deletes()) { $lines[] = get_string('usersdeleted', 'tool_uploaduser').': '.$this->deletes; $lines[] = get_string('deleteerrors', 'tool_uploaduser').': '.$this->deleteerrors; } if ($this->get_allow_renames()) { $lines[] = get_string('usersrenamed', 'tool_uploaduser').': '.$this->renames; $lines[] = get_string('renameerrors', 'tool_uploaduser').': '.$this->renameerrors; } if ($usersskipped = $this->usersskipped) { $lines[] = get_string('usersskipped', 'tool_uploaduser').': '.$usersskipped; } $lines[] = get_string('usersweakpassword', 'tool_uploaduser').': '.$this->weakpasswords; $lines[] = get_string('errors', 'tool_uploaduser').': '.$this->userserrors; return $lines; } }