├── example.csv ├── CHANGELOG.md ├── index.php ├── version.php ├── db └── access.php ├── classes ├── local │ ├── cli_progress_tracker.php │ ├── field_value_validators.php │ ├── cli_compact_progress_tracker.php │ └── text_progress_tracker.php ├── privacy │ └── provider.php ├── preview.php ├── cli_helper.php └── process.php ├── cli └── uploaduser.php ├── tests ├── field_value_validators_test.php └── cli_test.php ├── lang └── en │ └── tool_uploadusercli.php ├── README.md ├── locallib.php └── user_form.php /example.csv: -------------------------------------------------------------------------------- 1 | username,firstname,lastname,email 2 | student1,Student,One,s1@example.com 3 | student2,Student,Two,s2@example.com 4 | student3,Student,Three,s3@example.com -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## 2020121000 5 | ### Added 6 | - New parameter --compact that allows to output results in a compact format 7 | 8 | ### Changed 9 | - Renamed classes and global functions in locallib.php, user_form.php and tests/ 10 | to avoid conflicts with tool_uploaduser 11 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Bulk user registration script from a comma separated file 19 | * 20 | * @package tool 21 | * @subpackage uploadusercli 22 | * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | require('../../../config.php'); 27 | 28 | redirect(new moodle_url('/admin/tool/uploaduser/index.php')); -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Plugin version info 19 | * 20 | * @package tool 21 | * @subpackage uploadusercli 22 | * @copyright 2011 Petr Skoda {@link http://skodak.org} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | $plugin->version = 2020121000; // The current plugin version (Date: YYYYMMDDXX) 29 | $plugin->requires = 2019111800; // Requires this Moodle version 30 | $plugin->component = 'tool_uploadusercli'; // Full name of the plugin (used for diagnostics) 31 | -------------------------------------------------------------------------------- /db/access.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Defines the capabilities used by the user upload admin tool 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2013 Dan Poltawski 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $capabilities = array( 28 | 29 | // Allows the user to upload user pictures. 30 | 'tool/uploadusercli:uploaduserpictures' => array( 31 | 'riskbitmask' => RISK_SPAM, 32 | 'captype' => 'write', 33 | 'contextlevel' => CONTEXT_SYSTEM, 34 | 'archetypes' => array( 35 | 'manager' => CAP_ALLOW 36 | ), 37 | 'clonepermissionsfrom' => 'moodle/site:uploadusers', 38 | ), 39 | ); 40 | -------------------------------------------------------------------------------- /classes/local/cli_progress_tracker.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Class cli_progress_tracker 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli\local; 26 | 27 | /** 28 | * Tracks the progress of the user upload and outputs it in CLI script (writes to STDOUT) 29 | * 30 | * @package tool_uploadusercli 31 | * @copyright 2020 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class cli_progress_tracker extends text_progress_tracker { 35 | 36 | /** 37 | * Output one line (followed by newline) 38 | * @param string $line 39 | */ 40 | protected function output_line(string $line): void { 41 | cli_writeln($line); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /classes/privacy/provider.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Privacy Subsystem implementation for tool_uploadusercli. 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2018 Zig Tan 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli\privacy; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | /** 30 | * Privacy Subsystem for tool_uploadusercli implementing null_provider. 31 | * 32 | * @copyright 2018 Zig Tan 33 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 | */ 35 | class provider implements \core_privacy\local\metadata\null_provider { 36 | 37 | /** 38 | * Get the language string identifier with the component's language 39 | * file to explain why this plugin stores no data. 40 | * 41 | * @return string 42 | */ 43 | public static function get_reason() : string { 44 | return 'privacy:metadata'; 45 | } 46 | } -------------------------------------------------------------------------------- /cli/uploaduser.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * CLI script to upload users 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | define('CLI_SCRIPT', true); 26 | 27 | require_once(__DIR__ . '/../../../../config.php'); 28 | require_once($CFG->libdir . '/clilib.php'); 29 | 30 | if (moodle_needs_upgrading()) { 31 | cli_error("Moodle upgrade pending, export execution suspended."); 32 | } 33 | 34 | // Increase time and memory limit. 35 | core_php_time_limit::raise(); 36 | raise_memory_limit(MEMORY_EXTRA); 37 | 38 | // Emulate normal session - we use admin account by default, set language to the site language. 39 | cron_setup_user(); 40 | $USER->lang = $CFG->lang; 41 | 42 | $clihelper = new \tool_uploadusercli\cli_helper(); 43 | 44 | if ($clihelper->get_cli_option('help')) { 45 | $clihelper->print_help(); 46 | die(); 47 | } 48 | 49 | $clihelper->process(); 50 | 51 | if (!$clihelper->get_cli_option('compact')) { 52 | cli_separator(); 53 | foreach ($clihelper->get_stats() as $line) { 54 | cli_writeln($line); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /classes/local/field_value_validators.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * File containing the field_value_validators class. 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2019 Mathew May 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli\local; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | /** 30 | * Field validator class. 31 | * 32 | * @package tool_uploadusercli 33 | * @copyright 2019 Mathew May 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class field_value_validators { 37 | 38 | /** 39 | * List of valid and compatible themes. 40 | * 41 | * @return array 42 | */ 43 | protected static $themescache; 44 | 45 | /** 46 | * Validates the value provided for the theme field. 47 | * 48 | * @param string $value The value for the theme field. 49 | * @return array Contains the validation status and message. 50 | */ 51 | public static function validate_theme($value) { 52 | global $CFG; 53 | 54 | $status = 'normal'; 55 | $message = ''; 56 | 57 | // Validate if user themes are allowed. 58 | if (!$CFG->allowuserthemes) { 59 | $status = 'warning'; 60 | $message = get_string('userthemesnotallowed', 'tool_uploadusercli'); 61 | } else { 62 | // Cache list of themes if not yet set. 63 | if (!isset(self::$themescache)) { 64 | self::$themescache = get_list_of_themes(); 65 | } 66 | 67 | // Check if we have a valid theme. 68 | if (empty($value)) { 69 | $status = 'warning'; 70 | $message = get_string('notheme', 'tool_uploadusercli'); 71 | } else if (!isset(self::$themescache[$value])) { 72 | $status = 'warning'; 73 | $message = get_string('invalidtheme', 'tool_uploadusercli', s($value)); 74 | } 75 | } 76 | 77 | return [$status, $message]; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/field_value_validators_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Tests for field value validators of tool_uploadusercli. 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2019 Jun Pataleta 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | use tool_uploadusercli\local\field_value_validators; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | global $CFG; 30 | 31 | /** 32 | * Tests for field value validators of tool_uploadusercli. 33 | * 34 | * @package tool_uploadusercli 35 | * @copyright 2019 Jun Pataleta 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class tool_uploadusercli_field_value_validators_testcase extends advanced_testcase { 39 | 40 | /** 41 | * Data provider for \field_value_validators_testcase::test_validate_theme(). 42 | */ 43 | public function themes_provider() { 44 | return [ 45 | 'User themes disabled' => [ 46 | false, 'boost', 'warning', get_string('userthemesnotallowed', 'tool_uploadusercli') 47 | ], 48 | 'User themes enabled, empty theme' => [ 49 | true, '', 'warning', get_string('notheme', 'tool_uploadusercli') 50 | ], 51 | 'User themes enabled, invalid theme' => [ 52 | true, 'badtheme', 'warning', get_string('invalidtheme', 'tool_uploadusercli', 'badtheme') 53 | ], 54 | 'User themes enabled, valid theme' => [ 55 | true, 'boost', 'normal', '' 56 | ], 57 | ]; 58 | } 59 | 60 | /** 61 | * Unit test for \tool_uploadusercli\local\field_value_validators::validate_theme() 62 | * 63 | * @dataProvider themes_provider 64 | * @param boolean $userthemesallowed Whether to allow user themes. 65 | * @param string $themename The theme name to be tested. 66 | * @param string $expectedstatus The expected status. 67 | * @param string $expectedmessage The expected validation message. 68 | */ 69 | public function test_validate_theme($userthemesallowed, $themename, $expectedstatus, $expectedmessage) { 70 | $this->resetAfterTest(); 71 | 72 | // Set value for $CFG->allowuserthemes. 73 | set_config('allowuserthemes', $userthemesallowed); 74 | 75 | // Validate the theme. 76 | list($status, $message) = field_value_validators::validate_theme($themename); 77 | 78 | // Check the status and validation message. 79 | $this->assertEquals($expectedstatus, $status); 80 | $this->assertEquals($expectedmessage, $message); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /classes/local/cli_compact_progress_tracker.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Class cli_compact_progress_tracker 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli\local; 26 | 27 | /** 28 | * Tracks the progress of the user upload and outputs it in CLI script in compact format (writes to STDOUT) 29 | * 30 | * @package tool_uploadusercli 31 | * @copyright 2020 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class cli_compact_progress_tracker extends text_progress_tracker { 35 | 36 | /** 37 | * Output one line (followed by newline) 38 | * @param string $line 39 | */ 40 | protected function output_line(string $line): void { 41 | cli_writeln($line); 42 | } 43 | 44 | /** 45 | * Print table header. 46 | * @return void 47 | */ 48 | public function start() { 49 | parent::start(); 50 | $this->output_line('CSV Line | username | status'); 51 | } 52 | 53 | /** 54 | * Flush previous line and start a new one. 55 | * @return void 56 | */ 57 | public function flush() { 58 | if (empty($this->_row) or empty($this->_row['line']['normal'])) { 59 | // Nothing to print - each line has to have at least number. 60 | $this->_row = array(); 61 | foreach ($this->columns as $col) { 62 | $this->_row[$col] = ['normal' => '', 'info' => '', 'warning' => '', 'error' => '']; 63 | } 64 | return; 65 | } 66 | 67 | $lineno = get_string('linex', 'tool_uploadusercli', $this->_row['line']['normal']); 68 | $username = strip_tags($this->_row['username']['normal']); 69 | $statuses = []; 70 | $prefix = [ 71 | 'normal' => '', 72 | 'info' => '', 73 | 'warning' => get_string('warningprefix', 'tool_uploadusercli') . ' ', 74 | 'error' => get_string('errorprefix', 'tool_uploadusercli') . ' ', 75 | ]; 76 | foreach ($this->_row['status'] as $type => $content) { 77 | if (strlen($content)) { 78 | $statuses[] = $prefix[$type].$content; 79 | } 80 | } 81 | 82 | foreach ($this->_row as $key => $field) { 83 | foreach ($field as $type => $content) { 84 | if ($key !== 'status' && $type !== 'normal' && strlen($content)) { 85 | $statuses[] = $prefix[$type] . $this->headers[$key] . ': ' . 86 | str_replace("\n", "\n".str_repeat(" ", strlen($prefix[$type] . $this->headers[$key]) + 4), $content); 87 | } 88 | } 89 | } 90 | foreach ($this->columns as $col) { 91 | $this->_row[$col] = ['normal' => '', 'info' => '', 'warning' => '', 'error' => '']; 92 | } 93 | 94 | $this->output_line($lineno . ' | ' . $username . ' | ' . join('; ', $statuses)); 95 | } 96 | } -------------------------------------------------------------------------------- /classes/local/text_progress_tracker.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Class text_progress_tracker 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli\local; 26 | 27 | /** 28 | * Tracks the progress of the user upload and echos it in a text format 29 | * 30 | * @package tool_uploadusercli 31 | * @copyright 2020 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class text_progress_tracker extends \tool_uploadusercli_uu_progress_tracker { 35 | 36 | /** 37 | * Print table header. 38 | * @return void 39 | */ 40 | public function start() { 41 | $this->_row = null; 42 | } 43 | 44 | /** 45 | * Output one line (followed by newline) 46 | * @param string $line 47 | */ 48 | protected function output_line(string $line): void { 49 | echo $line . PHP_EOL; 50 | } 51 | 52 | /** 53 | * Flush previous line and start a new one. 54 | * @return void 55 | */ 56 | public function flush() { 57 | if (empty($this->_row) or empty($this->_row['line']['normal'])) { 58 | // Nothing to print - each line has to have at least number. 59 | $this->_row = array(); 60 | foreach ($this->columns as $col) { 61 | $this->_row[$col] = ['normal' => '', 'info' => '', 'warning' => '', 'error' => '']; 62 | } 63 | return; 64 | } 65 | $this->output_line(get_string('linex', 'tool_uploadusercli', $this->_row['line']['normal'])); 66 | $prefix = [ 67 | 'normal' => '', 68 | 'info' => '', 69 | 'warning' => get_string('warningprefix', 'tool_uploadusercli') . ' ', 70 | 'error' => get_string('errorprefix', 'tool_uploadusercli') . ' ', 71 | ]; 72 | foreach ($this->_row['status'] as $type => $content) { 73 | if (strlen($content)) { 74 | $this->output_line(' '.$prefix[$type].$content); 75 | } 76 | } 77 | 78 | foreach ($this->_row as $key => $field) { 79 | foreach ($field as $type => $content) { 80 | if ($key !== 'status' && $type !== 'normal' && strlen($content)) { 81 | $this->output_line(' ' . $prefix[$type] . $this->headers[$key] . ': ' . 82 | str_replace("\n", "\n".str_repeat(" ", strlen($prefix[$type] . $this->headers[$key]) + 4), $content)); 83 | } 84 | } 85 | } 86 | foreach ($this->columns as $col) { 87 | $this->_row[$col] = ['normal' => '', 'info' => '', 'warning' => '', 'error' => '']; 88 | } 89 | } 90 | 91 | /** 92 | * Add tracking info 93 | * @param string $col name of column 94 | * @param string $msg message 95 | * @param string $level 'normal', 'warning' or 'error' 96 | * @param bool $merge true means add as new line, false means override all previous text of the same type 97 | * @return void 98 | */ 99 | public function track($col, $msg, $level = 'normal', $merge = true) { 100 | if (empty($this->_row)) { 101 | $this->flush(); 102 | } 103 | if (!in_array($col, $this->columns)) { 104 | return; 105 | } 106 | if ($merge) { 107 | if ($this->_row[$col][$level] != '') { 108 | $this->_row[$col][$level] .= "\n"; 109 | } 110 | $this->_row[$col][$level] .= $msg; 111 | } else { 112 | $this->_row[$col][$level] = $msg; 113 | } 114 | } 115 | 116 | /** 117 | * Print the table end 118 | * @return void 119 | */ 120 | public function close() { 121 | $this->flush(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /classes/preview.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Class preview 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | use tool_uploadusercli\local\field_value_validators; 30 | 31 | require_once($CFG->libdir.'/csvlib.class.php'); 32 | require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploadusercli/locallib.php'); 33 | 34 | /** 35 | * Display the preview of a CSV file 36 | * 37 | * @package tool_uploadusercli 38 | * @copyright 2020 Marina Glancy 39 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 40 | */ 41 | class preview extends \html_table { 42 | 43 | /** @var \csv_import_reader */ 44 | protected $cir; 45 | /** @var array */ 46 | protected $filecolumns; 47 | /** @var int */ 48 | protected $previewrows; 49 | /** @var bool */ 50 | protected $noerror = true; // Keep status of any error. 51 | 52 | /** 53 | * preview constructor. 54 | * 55 | * @param \csv_import_reader $cir 56 | * @param array $filecolumns 57 | * @param int $previewrows 58 | * @throws \coding_exception 59 | */ 60 | public function __construct(\csv_import_reader $cir, array $filecolumns, int $previewrows) { 61 | parent::__construct(); 62 | $this->cir = $cir; 63 | $this->filecolumns = $filecolumns; 64 | $this->previewrows = $previewrows; 65 | 66 | $this->id = "uupreview"; 67 | $this->attributes['class'] = 'generaltable'; 68 | $this->tablealign = 'center'; 69 | $this->summary = get_string('uploaduserspreview', 'tool_uploadusercli'); 70 | $this->head = array(); 71 | $this->data = $this->read_data(); 72 | 73 | $this->head[] = get_string('uucsvline', 'tool_uploadusercli'); 74 | foreach ($filecolumns as $column) { 75 | $this->head[] = $column; 76 | } 77 | $this->head[] = get_string('status'); 78 | 79 | } 80 | 81 | /** 82 | * Read data 83 | * 84 | * @return array 85 | * @throws \coding_exception 86 | * @throws \dml_exception 87 | * @throws \moodle_exception 88 | */ 89 | protected function read_data() { 90 | global $DB, $CFG; 91 | 92 | $data = array(); 93 | $this->cir->init(); 94 | $linenum = 1; // Column header is first line. 95 | while ($linenum <= $this->previewrows and $fields = $this->cir->next()) { 96 | $linenum++; 97 | $rowcols = array(); 98 | $rowcols['line'] = $linenum; 99 | foreach ($fields as $key => $field) { 100 | $rowcols[$this->filecolumns[$key]] = s(trim($field)); 101 | } 102 | $rowcols['status'] = array(); 103 | 104 | if (isset($rowcols['username'])) { 105 | $stdusername = \core_user::clean_field($rowcols['username'], 'username'); 106 | if ($rowcols['username'] !== $stdusername) { 107 | $rowcols['status'][] = get_string('invalidusernameupload'); 108 | } 109 | if ($userid = $DB->get_field('user', 'id', 110 | ['username' => $stdusername, 'mnethostid' => $CFG->mnet_localhost_id])) { 111 | $rowcols['username'] = \html_writer::link( 112 | new \moodle_url('/user/profile.php', ['id' => $userid]), $rowcols['username']); 113 | } 114 | } else { 115 | $rowcols['status'][] = get_string('missingusername'); 116 | } 117 | 118 | if (isset($rowcols['email'])) { 119 | if (!validate_email($rowcols['email'])) { 120 | $rowcols['status'][] = get_string('invalidemail'); 121 | } 122 | 123 | $select = $DB->sql_like('email', ':email', false, true, false, '|'); 124 | $params = array('email' => $DB->sql_like_escape($rowcols['email'], '|')); 125 | if ($DB->record_exists_select('user', $select , $params)) { 126 | $rowcols['status'][] = get_string('useremailduplicate', 'error'); 127 | } 128 | } 129 | 130 | if (isset($rowcols['theme'])) { 131 | list($status, $message) = field_value_validators::validate_theme($rowcols['theme']); 132 | if ($status !== 'normal' && !empty($message)) { 133 | $rowcols['status'][] = $message; 134 | } 135 | } 136 | 137 | // Check if rowcols have custom profile field with correct data and update error state. 138 | $this->noerror = tool_uploadusercli_uu_check_custom_profile_data($rowcols) && $this->noerror; 139 | $rowcols['status'] = implode('
', $rowcols['status']); 140 | $data[] = $rowcols; 141 | } 142 | if ($fields = $this->cir->next()) { 143 | $data[] = array_fill(0, count($fields) + 2, '...'); 144 | } 145 | $this->cir->close(); 146 | 147 | return $data; 148 | } 149 | 150 | /** 151 | * Getter for noerror 152 | * 153 | * @return bool 154 | */ 155 | public function get_no_error() { 156 | return $this->noerror; 157 | } 158 | } -------------------------------------------------------------------------------- /lang/en/tool_uploadusercli.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Strings for component 'tool_uploadusercli', language 'en', branch 'MOODLE_22_STABLE' 19 | * 20 | * @package tool 21 | * @subpackage uploadusercli 22 | * @copyright 2011 Petr Skoda {@link http://skodak.org} 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | $string['allowdeletes'] = 'Allow deletes'; 27 | $string['allowrenames'] = 'Allow renames'; 28 | $string['allowsuspends'] = 'Allow suspending and activating of accounts'; 29 | $string['assignedsysrole'] = 'Assigned system role {$a}'; 30 | $string['clidefault'] = 'Default:'; 31 | $string['clierrorargument'] = 'Value for argument --{$a->name} is not valid. Allowed values: {$a->values}'; 32 | $string['clifile'] = 'Path to CSV file with the user data. Required.'; 33 | $string['clifilenotreadable'] = 'File {$a} does not exist or is not readable'; 34 | $string['clihelp'] = 'Print out this help.'; 35 | $string['climissingargument'] = 'Argument --{$a} is required'; 36 | $string['clioutputcompact'] = 'Output results in a compact format'; 37 | $string['clititle'] = 'Command line Upload user tool.'; 38 | $string['clivalidationerror'] = 'Validation error:'; 39 | $string['csvdelimiter'] = 'CSV delimiter'; 40 | $string['defaultvalues'] = 'Default values'; 41 | $string['deleteerrors'] = 'Delete errors'; 42 | $string['encoding'] = 'Encoding'; 43 | $string['errormnetadd'] = 'Can not add remote users'; 44 | $string['errorprefix'] = 'Error:'; 45 | $string['errors'] = 'Errors'; 46 | $string['examplecsv'] = 'Example text file'; 47 | $string['examplecsv_help'] = 'To use the example text file, download it then open it with a text or spreadsheet editor. Leave the first line unchanged, then edit the following lines (records) and add your user data, adding more lines as necessary. Save the file as CSV then upload it. 48 | 49 | The example text file may also be used for testing, as you are able to preview user data and can choose to cancel the action before user accounts are created.'; 50 | $string['infoprefix'] = 'Info:'; 51 | $string['invalidupdatetype'] = 'This option cannot be selected with the chosen upload type.'; 52 | $string['invaliduserdata'] = 'Invalid data detected for user {$a} and it has been automatically cleaned.'; 53 | $string['invalidtheme'] = 'Theme "{$a}" is not installed and will be ignored.'; 54 | $string['linex'] = 'Line {$a}'; 55 | $string['nochanges'] = 'No changes'; 56 | $string['notheme'] = 'No theme is defined for this user.'; 57 | $string['pluginname'] = 'User upload'; 58 | $string['renameerrors'] = 'Rename errors'; 59 | $string['requiredtemplate'] = 'Required. You may use template syntax here (%l = lastname, %f = firstname, %u = username). See help for details and examples.'; 60 | $string['rowpreviewnum'] = 'Preview rows'; 61 | $string['unassignedsysrole'] = 'Unassigned system role {$a}'; 62 | $string['userthemesnotallowed'] = 'User themes are not enabled, so any included in the upload users file will be ignored.'; 63 | $string['uploadpicture_baduserfield'] = 'The user attribute specified is not valid. Please, try again.'; 64 | $string['uploadpicture_cannotmovezip'] = 'Cannot move zip file to temporary directory.'; 65 | $string['uploadpicture_cannotprocessdir'] = 'Cannot process unzipped files.'; 66 | $string['uploadpicture_cannotsave'] = 'Cannot save picture for user {$a}. Check original picture file.'; 67 | $string['uploadpicture_cannotunzip'] = 'Cannot unzip pictures file.'; 68 | $string['uploadpicture_invalidfilename'] = 'Picture file {$a} has invalid characters in its name. Skipping.'; 69 | $string['uploadpicture_overwrite'] = 'Overwrite existing user pictures?'; 70 | $string['uploadpicture_userfield'] = 'User attribute to use to match pictures:'; 71 | $string['uploadpicture_usernotfound'] = 'User with a \'{$a->userfield}\' value of \'{$a->uservalue}\' does not exist. Skipping.'; 72 | $string['uploadpicture_userskipped'] = 'Skipping user {$a} (already has a picture).'; 73 | $string['uploadpicture_userupdated'] = 'Picture updated for user {$a}.'; 74 | $string['uploadpictures'] = 'Upload user pictures'; 75 | $string['uploadpictures_help'] = 'User pictures can be uploaded as a zip file of image files. The image files should be named chosen-user-attribute.extension, for example user1234.jpg for a user with username user1234.'; 76 | $string['uploadusers'] = 'Upload users'; 77 | $string['uploadusers_help'] = 'Users may be uploaded (and optionally enrolled in courses) via text file. The format of the file should be as follows: 78 | 79 | * Each line of the file contains one record 80 | * Each record is a series of data separated by commas (or other delimiters) 81 | * The first record contains a list of fieldnames defining the format of the rest of the file 82 | * Required fieldnames are username, password, firstname, lastname, email'; 83 | $string['uploadusers_link'] = 'admin/tool/uploadusercli/index'; 84 | $string['uploaduserspreview'] = 'Upload users preview'; 85 | $string['uploadusersresult'] = 'Upload users results'; 86 | $string['useraccountupdated'] = 'User updated'; 87 | $string['useraccountuptodate'] = 'User up-to-date'; 88 | $string['userdeleted'] = 'User deleted'; 89 | $string['userrenamed'] = 'User renamed'; 90 | $string['userscreated'] = 'Users created'; 91 | $string['usersdeleted'] = 'Users deleted'; 92 | $string['usersrenamed'] = 'Users renamed'; 93 | $string['usersskipped'] = 'Users skipped'; 94 | $string['usersupdated'] = 'Users updated'; 95 | $string['usersweakpassword'] = 'Users having a weak password'; 96 | $string['uubulk'] = 'Select for bulk user actions'; 97 | $string['uubulkall'] = 'All users'; 98 | $string['uubulknew'] = 'New users'; 99 | $string['uubulkupdated'] = 'Updated users'; 100 | $string['uucsvline'] = 'CSV line'; 101 | $string['uulegacy1role'] = '(Original Student) typeN=1'; 102 | $string['uulegacy2role'] = '(Original Teacher) typeN=2'; 103 | $string['uulegacy3role'] = '(Original Non-editing teacher) typeN=3'; 104 | $string['uunoemailduplicates'] = 'Prevent email address duplicates'; 105 | $string['uuoptype'] = 'Upload type'; 106 | $string['uuoptype_addinc'] = 'Add all, append number to usernames if needed'; 107 | $string['uuoptype_addnew'] = 'Add new only, skip existing users'; 108 | $string['uuoptype_addupdate'] = 'Add new and update existing users'; 109 | $string['uuoptype_update'] = 'Update existing users only'; 110 | $string['uupasswordcron'] = 'Generated in cron'; 111 | $string['uupasswordnew'] = 'New user password'; 112 | $string['uupasswordold'] = 'Existing user password'; 113 | $string['uustandardusernames'] = 'Standardise usernames'; 114 | $string['uuupdateall'] = 'Override with file and defaults'; 115 | $string['uuupdatefromfile'] = 'Override with file'; 116 | $string['uuupdatemissing'] = 'Fill in missing from file and defaults'; 117 | $string['uuupdatetype'] = 'Existing user details'; 118 | $string['uuusernametemplate'] = 'Username template'; 119 | $string['privacy:metadata'] = 'The User upload plugin does not store any personal data.'; 120 | $string['warningprefix'] = 'Warning:'; 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Upload user (CLI only) 2 | ====================== 3 | 4 | This plugin allows to use CLI script to upload user in Moodle 3.8 and Moodle 3.9. This functionality 5 | is included in the standard distribution of Moodle 3.10. 6 | 7 | To print help execute: 8 | ``` 9 | sudo -u www-data php admin/tool/uploadusercli/cli/uploaduser.php --help 10 | ``` 11 | 12 | Output of the help command on the default installation (some defaults may be different on your site): 13 | ``` 14 | Command line Upload user tool. 15 | 16 | Options: 17 | -h, --help Print out this help. 18 | --file=PATH Path to CSV file with the user data. Required. 19 | --compact Output results in a compact format 20 | --delimiter_name=VALUE CSV delimiter: 21 | comma - , 22 | semicolon - ; 23 | colon - : 24 | tab - \t 25 | Default: comma 26 | --encoding=VALUE Encoding 27 | Default: UTF-8 28 | --uutype=VALUE Upload type: 29 | 0 - Add new only, skip existing users 30 | 1 - Add all, append number to usernames if 31 | needed 32 | 2 - Add new and update existing users 33 | 3 - Update existing users only 34 | Default: 0 35 | --uupasswordnew=VALUE New user password: 36 | 0 - Field required in file 37 | 1 - Create password if needed and send via email 38 | Default: 1 39 | --uuupdatetype=VALUE Existing user details: 40 | 0 - No changes 41 | 1 - Override with file 42 | 2 - Override with file and defaults 43 | 3 - Fill in missing from file and defaults 44 | Default: 0 45 | --uupasswordold=VALUE Existing user password: 46 | 0 - No changes 47 | 1 - Update 48 | Default: 0 49 | --uuforcepasswordchange=VALUE Force password change: 50 | 1 - Users having a weak password 51 | 0 - None 52 | 2 - All 53 | Default: 1 54 | --uuallowrenames=VALUE Allow renames: 55 | 0 - No 56 | 1 - Yes 57 | Default: 0 58 | --uuallowdeletes=VALUE Allow deletes: 59 | 0 - No 60 | 1 - Yes 61 | Default: 0 62 | --uuallowsuspends=VALUE Allow suspending and activating of accounts: 63 | 0 - No 64 | 1 - Yes 65 | Default: 1 66 | --uustandardusernames=VALUE Standardise usernames: 67 | 0 - No 68 | 1 - Yes 69 | Default: 1 70 | --uulegacy1=VALUE (Original Student) typeN=1: 71 | 5 - Student (student) 72 | 4 - Non-editing teacher (teacher) 73 | 3 - Teacher (editingteacher) 74 | 1 - Manager (manager) 75 | Default: 5 76 | --uulegacy2=VALUE (Original Teacher) typeN=2: 77 | 5 - Student (student) 78 | 4 - Non-editing teacher (teacher) 79 | 3 - Teacher (editingteacher) 80 | 1 - Manager (manager) 81 | Default: 3 82 | --uulegacy3=VALUE (Original Non-editing teacher) typeN=3: 83 | 5 - Student (student) 84 | 4 - Non-editing teacher (teacher) 85 | 3 - Teacher (editingteacher) 86 | 1 - Manager (manager) 87 | Default: 4 88 | --username=VALUE Username template 89 | --email=VALUE Email address 90 | --auth=VALUE Choose an authentication method: 91 | manual - Manual accounts 92 | nologin - No login 93 | email - Email-based self-registration 94 | Default: manual 95 | --maildisplay=VALUE Email display: 96 | 0 - Hide my email address from non-privileged 97 | users 98 | 1 - Allow everyone to see my email address 99 | 2 - Allow only other course members to see my 100 | email address 101 | Default: 2 102 | --emailstop=VALUE Disable notifications: 103 | 0 - This email address is enabled 104 | 1 - This email address is disabled 105 | Default: 0 106 | --mailformat=VALUE Email format: 107 | 0 - Plain text format 108 | 1 - Pretty HTML format 109 | Default: 1 110 | --maildigest=VALUE Email digest type: 111 | 0 - No digest (single email per forum post) 112 | 1 - Complete (daily email with full posts) 113 | 2 - Subjects (daily email with subjects only) 114 | Default: 0 115 | --autosubscribe=VALUE Forum auto-subscribe: 116 | 1 - Yes: when I post, subscribe me to that forum 117 | discussion 118 | 0 - No: don't automatically subscribe me to 119 | forum discussions 120 | Default: 1 121 | --city=VALUE City/town 122 | --country=VALUE Select a country 123 | --timezone=VALUE Timezone 124 | Default: Europe/Berlin 125 | --lang=VALUE Preferred language: 126 | en - English ‎(en)‎ 127 | Default: en 128 | --description=VALUE Description 129 | --url=VALUE Web page 130 | --idnumber=VALUE ID number 131 | --institution=VALUE Institution 132 | --department=VALUE Department 133 | --phone1=VALUE Phone 134 | --phone2=VALUE Mobile phone 135 | --address=VALUE Address 136 | 137 | Example: 138 | $sudo -u www-data /usr/bin/php admin/tool/uploadusercli/cli/uploadusercli.php --file=PATH 139 | ``` 140 | 141 | Example output: 142 | --------------- 143 | 144 | When used with the file [example.csv](example.csv) included in this plugin: 145 | 146 | ``` 147 | $ sudo -u www-data php admin/tool/uploadusercli/cli/uploaduser.php --file=../example.csv 148 | Line 2 149 | New user 150 | Warning: Password: Generated in cron 151 | Line 3 152 | New user 153 | Warning: Password: Generated in cron 154 | Line 4 155 | New user 156 | Warning: Password: Generated in cron 157 | 158 | ``` 159 | 160 | Documentation 161 | ------------- 162 | 163 | https://docs.moodle.org/310/en/Upload_users - documentation for Moodle LMS version 3.10 164 | -------------------------------------------------------------------------------- /tests/cli_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Tests for CLI tool_uploadusercli. 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | use \tool_uploadusercli\cli_helper; 26 | 27 | /** 28 | * Tests for CLI tool_uploadusercli. 29 | * 30 | * @package tool_uploadusercli 31 | * @copyright 2020 Marina Glancy 32 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 33 | */ 34 | class tool_uploadusercli_cli_testcase extends advanced_testcase { 35 | 36 | /** 37 | * Generate cli_helper and mock $_SERVER['argv'] 38 | * 39 | * @param array $mockargv 40 | * @return \tool_uploadusercli\cli_helper 41 | */ 42 | protected function construct_helper(array $mockargv = []) { 43 | if (array_key_exists('argv', $_SERVER)) { 44 | $oldservervars = $_SERVER['argv']; 45 | } 46 | $_SERVER['argv'] = array_merge([''], $mockargv); 47 | $clihelper = new cli_helper(\tool_uploadusercli\local\text_progress_tracker::class); 48 | if (isset($oldservervars)) { 49 | $_SERVER['argv'] = $oldservervars; 50 | } else { 51 | unset($_SERVER['argv']); 52 | } 53 | return $clihelper; 54 | } 55 | 56 | /** 57 | * Tests simple upload with course enrolment and group allocation 58 | */ 59 | public function test_upload_with_course_enrolment() { 60 | global $CFG; 61 | $this->resetAfterTest(); 62 | set_config('passwordpolicy', 0); 63 | $this->setAdminUser(); 64 | 65 | $course = $this->getDataGenerator()->create_course(['fullname' => 'Maths', 'shortname' => 'math102']); 66 | $g1 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 1', 'idnumber' => 'S1']); 67 | $g2 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 3', 'idnumber' => 'S3']); 68 | 69 | $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users.csv'; 70 | 71 | $clihelper = $this->construct_helper(["--file=$filepath"]); 72 | ob_start(); 73 | $clihelper->process(); 74 | $output = ob_get_contents(); 75 | ob_end_clean(); 76 | 77 | // CLI output suggests that 2 users were created. 78 | $stats = $clihelper->get_stats(); 79 | $this->assertEquals(2, preg_match_all('/New user/', $output)); 80 | $this->assertEquals('Users created: 2', $stats[0]); 81 | 82 | // Tom Jones and Trent Reznor are enrolled into the course, first one to group $g1 and second to group $g2. 83 | $enrols = array_values(enrol_get_course_users($course->id)); 84 | $this->assertEqualsCanonicalizing(['reznor', 'jonest'], [$enrols[0]->username, $enrols[1]->username]); 85 | $g1members = groups_get_groups_members($g1->id); 86 | $this->assertEquals(1, count($g1members)); 87 | $this->assertEquals('Jones', $g1members[key($g1members)]->lastname); 88 | $g2members = groups_get_groups_members($g2->id); 89 | $this->assertEquals(1, count($g2members)); 90 | $this->assertEquals('Reznor', $g2members[key($g2members)]->lastname); 91 | } 92 | 93 | /** 94 | * Test applying defaults during the user upload 95 | */ 96 | public function test_upload_with_applying_defaults() { 97 | global $CFG; 98 | $this->resetAfterTest(); 99 | set_config('passwordpolicy', 0); 100 | $this->setAdminUser(); 101 | 102 | $course = $this->getDataGenerator()->create_course(['fullname' => 'Maths', 'shortname' => 'math102']); 103 | $g1 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 1', 'idnumber' => 'S1']); 104 | $g2 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 3', 'idnumber' => 'S3']); 105 | 106 | $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users.csv'; 107 | 108 | $clihelper = $this->construct_helper(["--file=$filepath", '--city=Brighton', '--department=Purchasing']); 109 | ob_start(); 110 | $clihelper->process(); 111 | $output = ob_get_contents(); 112 | ob_end_clean(); 113 | 114 | // CLI output suggests that 2 users were created. 115 | $stats = $clihelper->get_stats(); 116 | $this->assertEquals(2, preg_match_all('/New user/', $output)); 117 | $this->assertEquals('Users created: 2', $stats[0]); 118 | 119 | // Users have default values applied. 120 | $user1 = core_user::get_user_by_username('jonest'); 121 | $this->assertEquals('Brighton', $user1->city); 122 | $this->assertEquals('Purchasing', $user1->department); 123 | } 124 | 125 | /** 126 | * User upload with user profile fields 127 | */ 128 | public function test_upload_with_profile_fields() { 129 | global $DB, $CFG; 130 | $this->resetAfterTest(); 131 | set_config('passwordpolicy', 0); 132 | $this->setAdminUser(); 133 | 134 | $categoryid = $DB->insert_record('user_info_category', ['name' => 'Cat 1', 'sortorder' => 1]); 135 | $this->field1 = $DB->insert_record('user_info_field', [ 136 | 'shortname' => 'superfield', 'name' => 'Super field', 'categoryid' => $categoryid, 137 | 'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1]); 138 | 139 | $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users_profile.csv'; 140 | 141 | $clihelper = $this->construct_helper(["--file=$filepath"]); 142 | ob_start(); 143 | $clihelper->process(); 144 | $output = ob_get_contents(); 145 | ob_end_clean(); 146 | 147 | // CLI output suggests that 2 users were created. 148 | $stats = $clihelper->get_stats(); 149 | $this->assertEquals(2, preg_match_all('/New user/', $output)); 150 | $this->assertEquals('Users created: 2', $stats[0]); 151 | 152 | // Created users have data in the profile fields. 153 | $user1 = core_user::get_user_by_username('reznort'); 154 | $profilefields1 = profile_user_record($user1->id); 155 | $this->assertEquals((object)['superfield' => 'Loves cats'], $profilefields1); 156 | } 157 | 158 | /** 159 | * Testing that help for CLI does not throw errors 160 | */ 161 | public function test_cli_help() { 162 | $this->resetAfterTest(); 163 | $this->setAdminUser(); 164 | $clihelper = $this->construct_helper(["--help"]); 165 | ob_start(); 166 | $clihelper->print_help(); 167 | $output = ob_get_contents(); 168 | ob_end_clean(); 169 | 170 | // Basically a test that everything can be parsed and displayed without errors. Check that some options are present. 171 | $this->assertEquals(1, preg_match('/--delimiter_name=VALUE/', $output)); 172 | $this->assertEquals(1, preg_match('/--uutype=VALUE/', $output)); 173 | $this->assertEquals(1, preg_match('/--auth=VALUE/', $output)); 174 | } 175 | 176 | /** 177 | * Testing skipped user when one exists 178 | */ 179 | public function test_create_when_user_exists() { 180 | global $CFG; 181 | $this->resetAfterTest(); 182 | set_config('passwordpolicy', 0); 183 | $this->setAdminUser(); 184 | 185 | $course = $this->getDataGenerator()->create_course(['fullname' => 'Maths', 'shortname' => 'math102']); 186 | $g1 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 1', 'idnumber' => 'S1']); 187 | $g2 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 3', 'idnumber' => 'S3']); 188 | 189 | // Create a user with username jonest. 190 | $user1 = $this->getDataGenerator()->create_user(['username' => 'jonest', 'email' => 'jonest@someplace.edu']); 191 | 192 | $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users.csv'; 193 | 194 | $clihelper = $this->construct_helper(["--file=$filepath"]); 195 | ob_start(); 196 | $clihelper->process(); 197 | $output = ob_get_contents(); 198 | ob_end_clean(); 199 | 200 | // CLI output suggests that 1 user was created and 1 skipped. 201 | $stats = $clihelper->get_stats(); 202 | $this->assertEquals(1, preg_match_all('/New user/', $output)); 203 | $this->assertEquals('Users created: 1', $stats[0]); 204 | $this->assertEquals('Users skipped: 1', $stats[1]); 205 | 206 | // Trent Reznor is enrolled into the course, Tom Jones is not! 207 | $enrols = array_values(enrol_get_course_users($course->id)); 208 | $this->assertEqualsCanonicalizing(['reznor'], [$enrols[0]->username]); 209 | } 210 | 211 | /** 212 | * Testing update mode - do not update user records but allow enrolments 213 | */ 214 | public function test_enrolments_when_user_exists() { 215 | global $CFG; 216 | require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploadusercli/locallib.php'); 217 | 218 | $this->resetAfterTest(); 219 | set_config('passwordpolicy', 0); 220 | $this->setAdminUser(); 221 | 222 | $course = $this->getDataGenerator()->create_course(['fullname' => 'Maths', 'shortname' => 'math102']); 223 | $g1 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 1', 'idnumber' => 'S1']); 224 | $g2 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 3', 'idnumber' => 'S3']); 225 | 226 | // Create a user with username jonest. 227 | $this->getDataGenerator()->create_user(['username' => 'jonest', 'email' => 'jonest@someplace.edu', 228 | 'firstname' => 'OLDNAME']); 229 | 230 | $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users.csv'; 231 | 232 | $clihelper = $this->construct_helper(["--file=$filepath", '--uutype='.UU_USER_UPDATE]); 233 | ob_start(); 234 | $clihelper->process(); 235 | $output = ob_get_contents(); 236 | ob_end_clean(); 237 | 238 | // CLI output suggests that 1 user was created and 1 skipped. 239 | $stats = $clihelper->get_stats(); 240 | $this->assertEquals(0, preg_match_all('/New user/', $output)); 241 | $this->assertEquals('Users updated: 0', $stats[0]); 242 | $this->assertEquals('Users skipped: 1', $stats[1]); 243 | 244 | // Tom Jones is enrolled into the course. 245 | $enrols = array_values(enrol_get_course_users($course->id)); 246 | $this->assertEqualsCanonicalizing(['jonest'], [$enrols[0]->username]); 247 | // User reznor is not created. 248 | $this->assertFalse(core_user::get_user_by_username('reznor')); 249 | // User jonest is not updated. 250 | $this->assertEquals('OLDNAME', core_user::get_user_by_username('jonest')->firstname); 251 | } 252 | 253 | /** 254 | * Testing update mode - update user records and perform enrolments. 255 | */ 256 | public function test_udpate_user() { 257 | global $CFG; 258 | require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploadusercli/locallib.php'); 259 | 260 | $this->resetAfterTest(); 261 | set_config('passwordpolicy', 0); 262 | $this->setAdminUser(); 263 | 264 | $course = $this->getDataGenerator()->create_course(['fullname' => 'Maths', 'shortname' => 'math102']); 265 | $g1 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 1', 'idnumber' => 'S1']); 266 | $g2 = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'Section 3', 'idnumber' => 'S3']); 267 | 268 | // Create a user with username jonest. 269 | $this->getDataGenerator()->create_user(['username' => 'jonest', 270 | 'email' => 'jonest@someplace.edu', 'firstname' => 'OLDNAME']); 271 | 272 | $filepath = $CFG->dirroot.'/lib/tests/fixtures/upload_users.csv'; 273 | 274 | $clihelper = $this->construct_helper(["--file=$filepath", '--uutype='.UU_USER_UPDATE, 275 | '--uuupdatetype='.UU_UPDATE_FILEOVERRIDE]); 276 | ob_start(); 277 | $clihelper->process(); 278 | $output = ob_get_contents(); 279 | ob_end_clean(); 280 | 281 | // CLI output suggests that 1 user was created and 1 skipped. 282 | $stats = $clihelper->get_stats(); 283 | $this->assertEquals(0, preg_match_all('/New user/', $output)); 284 | $this->assertEquals('Users updated: 1', $stats[0]); 285 | $this->assertEquals('Users skipped: 1', $stats[1]); 286 | 287 | // Tom Jones is enrolled into the course. 288 | $enrols = array_values(enrol_get_course_users($course->id)); 289 | $this->assertEqualsCanonicalizing(['jonest'], [$enrols[0]->username]); 290 | // User reznor is not created. 291 | $this->assertFalse(core_user::get_user_by_username('reznor')); 292 | // User jonest is updated, new first name is Tom. 293 | $this->assertEquals('Tom', core_user::get_user_by_username('jonest')->firstname); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /classes/cli_helper.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Class cli_helper 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Marina Glancy 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | use tool_uploadusercli\local\cli_compact_progress_tracker; 30 | use tool_uploadusercli\local\cli_progress_tracker; 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/uploadusercli/locallib.php'); 38 | require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploadusercli/user_form.php'); 39 | require_once($CFG->libdir . '/clilib.php'); 40 | 41 | /** 42 | * Helper method for CLI script to upload users (also has special wrappers for cli* functions for phpunit testing) 43 | * 44 | * @package tool_uploadusercli 45 | * @copyright 2020 Marina Glancy 46 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 | */ 48 | class cli_helper { 49 | 50 | /** @var string */ 51 | protected $operation; 52 | /** @var array */ 53 | protected $clioptions; 54 | /** @var array */ 55 | protected $unrecognized; 56 | /** @var string */ 57 | protected $progresstrackerclass; 58 | 59 | /** @var process */ 60 | protected $process; 61 | 62 | /** 63 | * cli_helper constructor. 64 | * 65 | * @param string|null $progresstrackerclass 66 | */ 67 | public function __construct(?string $progresstrackerclass = null) { 68 | $this->progresstrackerclass = $progresstrackerclass ?? cli_progress_tracker::class; 69 | $optionsdefinitions = $this->options_definitions(); 70 | $longoptions = []; 71 | $shortmapping = []; 72 | foreach ($optionsdefinitions as $key => $option) { 73 | $longoptions[$key] = $option['default']; 74 | if (!empty($option['alias'])) { 75 | $shortmapping[$option['alias']] = $key; 76 | } 77 | } 78 | 79 | list($this->clioptions, $this->unrecognized) = cli_get_params( 80 | $longoptions, 81 | $shortmapping 82 | ); 83 | } 84 | 85 | /** 86 | * Options used in this CLI script 87 | * 88 | * @return array 89 | */ 90 | protected function options_definitions(): array { 91 | $options = [ 92 | 'help' => [ 93 | 'hasvalue' => false, 94 | 'description' => get_string('clihelp', 'tool_uploadusercli'), 95 | 'default' => 0, 96 | 'alias' => 'h', 97 | ], 98 | 'file' => [ 99 | 'hasvalue' => 'PATH', 100 | 'description' => get_string('clifile', 'tool_uploadusercli'), 101 | 'default' => null, 102 | 'validation' => function($file) { 103 | if (!$file) { 104 | $this->cli_error(get_string('climissingargument', 'tool_uploadusercli', 'file')); 105 | } 106 | if ($file && (!file_exists($file) || !is_readable($file))) { 107 | $this->cli_error(get_string('clifilenotreadable', 'tool_uploadusercli', $file)); 108 | } 109 | } 110 | ], 111 | 'compact' => [ 112 | 'hasvalue' => false, 113 | 'description' => get_string('clioutputcompact', 'tool_uploadusercli'), 114 | 'default' => false 115 | ], 116 | ]; 117 | $form = new \tool_uploadusercli_admin_uploaduser_form1(); 118 | [$elements, $defaults] = $form->get_form_for_cli(); 119 | $options += $this->prepare_form_elements_for_cli($elements, $defaults); 120 | $form = new \tool_uploadusercli_admin_uploaduser_form2(null, ['columns' => ['type1'], 'data' => []]); 121 | [$elements, $defaults] = $form->get_form_for_cli(); 122 | $options += $this->prepare_form_elements_for_cli($elements, $defaults); 123 | return $options; 124 | } 125 | 126 | /** 127 | * Print help for export 128 | */ 129 | public function print_help(): void { 130 | $this->cli_writeln(get_string('clititle', 'tool_uploadusercli')); 131 | $this->cli_writeln(''); 132 | $this->print_help_options($this->options_definitions()); 133 | $this->cli_writeln(''); 134 | $this->cli_writeln('Example:'); 135 | $this->cli_writeln('$sudo -u www-data /usr/bin/php admin/tool/uploadusercli/cli/uploadusercli.php --file=PATH'); 136 | } 137 | 138 | /** 139 | * Get CLI option 140 | * 141 | * @param string $key 142 | * @return mixed|null 143 | */ 144 | public function get_cli_option(string $key) { 145 | return $this->clioptions[$key] ?? null; 146 | } 147 | 148 | /** 149 | * Write a text to the given stream 150 | * 151 | * @param string $text text to be written 152 | */ 153 | protected function cli_write($text): void { 154 | if (PHPUNIT_TEST) { 155 | echo $text; 156 | } else { 157 | cli_write($text); 158 | } 159 | } 160 | 161 | /** 162 | * Write error notification 163 | * @param string $text 164 | * @return void 165 | */ 166 | protected function cli_problem($text): void { 167 | if (PHPUNIT_TEST) { 168 | echo $text; 169 | } else { 170 | cli_problem($text); 171 | } 172 | } 173 | 174 | /** 175 | * Write a text followed by an end of line symbol to the given stream 176 | * 177 | * @param string $text text to be written 178 | */ 179 | protected function cli_writeln($text): void { 180 | $this->cli_write($text . PHP_EOL); 181 | } 182 | 183 | /** 184 | * Write to standard error output and exit with the given code 185 | * 186 | * @param string $text 187 | * @param int $errorcode 188 | * @return void (does not return) 189 | */ 190 | protected function cli_error($text, $errorcode = 1): void { 191 | $this->cli_problem($text); 192 | $this->die($errorcode); 193 | } 194 | 195 | /** 196 | * Wrapper for "die()" method so we can unittest it 197 | * 198 | * @param mixed $errorcode 199 | * @throws \moodle_exception 200 | */ 201 | protected function die($errorcode): void { 202 | if (!PHPUNIT_TEST) { 203 | die($errorcode); 204 | } else { 205 | throw new \moodle_exception('CLI script finished with error code '.$errorcode); 206 | } 207 | } 208 | 209 | /** 210 | * Display as CLI table 211 | * 212 | * @param array $column1 213 | * @param array $column2 214 | * @param int $indent 215 | * @return string 216 | */ 217 | protected function convert_to_table(array $column1, array $column2, int $indent = 0): string { 218 | $maxlengthleft = 0; 219 | $left = []; 220 | $column1 = array_values($column1); 221 | $column2 = array_values($column2); 222 | foreach ($column1 as $i => $l) { 223 | $left[$i] = str_repeat(' ', $indent) . $l; 224 | if (strlen('' . $column2[$i])) { 225 | $maxlengthleft = max($maxlengthleft, strlen($l) + $indent); 226 | } 227 | } 228 | $maxlengthright = 80 - $maxlengthleft - 1; 229 | $output = ''; 230 | foreach ($column2 as $i => $r) { 231 | if (!strlen('' . $r)) { 232 | $output .= $left[$i] . "\n"; 233 | continue; 234 | } 235 | $right = wordwrap($r, $maxlengthright, "\n"); 236 | $output .= str_pad($left[$i], $maxlengthleft) . ' ' . 237 | str_replace("\n", PHP_EOL . str_repeat(' ', $maxlengthleft + 1), $right) . PHP_EOL; 238 | } 239 | return $output; 240 | } 241 | 242 | /** 243 | * Display available CLI options as a table 244 | * 245 | * @param array $options 246 | */ 247 | protected function print_help_options(array $options): void { 248 | $left = []; 249 | $right = []; 250 | foreach ($options as $key => $option) { 251 | if ($option['hasvalue'] !== false) { 252 | $l = "--$key={$option['hasvalue']}"; 253 | } else if (!empty($option['alias'])) { 254 | $l = "-{$option['alias']}, --$key"; 255 | } else { 256 | $l = "--$key"; 257 | } 258 | $left[] = $l; 259 | $right[] = $option['description']; 260 | } 261 | $this->cli_write('Options:' . PHP_EOL . $this->convert_to_table($left, $right)); 262 | } 263 | 264 | /** 265 | * Process the upload 266 | */ 267 | public function process(): void { 268 | // First, validate all arguments. 269 | $definitions = $this->options_definitions(); 270 | foreach ($this->clioptions as $key => $value) { 271 | if ($validator = $definitions[$key]['validation'] ?? null) { 272 | $validator($value); 273 | } 274 | } 275 | 276 | // Read the CSV file. 277 | $iid = \csv_import_reader::get_new_iid('uploadusercli'); 278 | $cir = new \csv_import_reader($iid, 'uploadusercli'); 279 | $cir->load_csv_content(file_get_contents($this->get_cli_option('file')), 280 | $this->get_cli_option('encoding'), $this->get_cli_option('delimiter_name')); 281 | $csvloaderror = $cir->get_error(); 282 | 283 | if (!is_null($csvloaderror)) { 284 | $this->cli_error(get_string('csvloaderror', 'error', $csvloaderror), 1); 285 | } 286 | 287 | // Start upload user process. 288 | $progresstrackerclass = $this->get_cli_option('compact') ? cli_compact_progress_tracker::class : $this->progresstrackerclass; 289 | $this->process = new \tool_uploadusercli\process($cir, $progresstrackerclass); 290 | $filecolumns = $this->process->get_file_columns(); 291 | 292 | $form = $this->mock_form(['columns' => $filecolumns, 'data' => ['iid' => $iid, 'previewrows' => 1]], $this->clioptions); 293 | 294 | if (!$form->is_validated()) { 295 | $errors = $form->get_validation_errors(); 296 | $this->cli_error(get_string('clivalidationerror', 'tool_uploadusercli') . PHP_EOL . 297 | $this->convert_to_table(array_keys($errors), array_values($errors), 2)); 298 | } 299 | 300 | $this->process->set_form_data($form->get_data()); 301 | $this->process->process(); 302 | } 303 | 304 | /** 305 | * Mock form submission 306 | * 307 | * @param array $customdata 308 | * @param array $submitteddata 309 | * @return \tool_uploadusercli_admin_uploaduser_form2 310 | */ 311 | protected function mock_form(array $customdata, array $submitteddata): \tool_uploadusercli_admin_uploaduser_form2 { 312 | global $USER; 313 | $submitteddata['description'] = ['text' => $submitteddata['description'], 'format' => FORMAT_HTML]; 314 | 315 | // Now mock the form submission. 316 | $submitteddata['_qf__tool_uploadusercli_admin_uploaduser_form2'] = 1; 317 | $oldignoresesskey = $USER->ignoresesskey ?? null; 318 | $USER->ignoresesskey = true; 319 | $form = new \tool_uploadusercli_admin_uploaduser_form2(null, $customdata, 'post', '', [], true, $submitteddata); 320 | $USER->ignoresesskey = $oldignoresesskey; 321 | 322 | $form->set_data($submitteddata); 323 | return $form; 324 | } 325 | 326 | /** 327 | * Prepare form elements for CLI 328 | * 329 | * @param \HTML_QuickForm_element[] $elements 330 | * @param array $defaults 331 | * @return array 332 | */ 333 | protected function prepare_form_elements_for_cli(array $elements, array $defaults): array { 334 | $options = []; 335 | foreach ($elements as $element) { 336 | if ($element instanceof \HTML_QuickForm_submit || $element instanceof \HTML_QuickForm_static) { 337 | continue; 338 | } 339 | $type = $element->getType(); 340 | if ($type === 'html' || $type === 'hidden' || $type === 'header') { 341 | continue; 342 | } 343 | 344 | $name = $element->getName(); 345 | if ($name === null || preg_match('/^mform_isexpanded_/', $name) 346 | || preg_match('/^_qf__/', $name)) { 347 | continue; 348 | } 349 | 350 | $label = $element->getLabel(); 351 | if (!strlen($label) && method_exists($element, 'getText')) { 352 | $label = $element->getText(); 353 | } 354 | $default = $defaults[$element->getName()] ?? null; 355 | 356 | $postfix = ''; 357 | $possiblevalues = null; 358 | if ($element instanceof \HTML_QuickForm_select) { 359 | $selectoptions = $element->_options; 360 | $possiblevalues = []; 361 | foreach ($selectoptions as $option) { 362 | $possiblevalues[] = '' . $option['attr']['value']; 363 | } 364 | if (count($selectoptions) < 10) { 365 | $postfix .= ':'; 366 | foreach ($selectoptions as $option) { 367 | $postfix .= "\n ".$option['attr']['value']." - ".$option['text']; 368 | } 369 | } 370 | if (!array_key_exists($name, $defaults)) { 371 | $firstoption = reset($selectoptions); 372 | $default = $firstoption['attr']['value']; 373 | } 374 | } 375 | 376 | if ($element instanceof \HTML_QuickForm_checkbox) { 377 | $postfix = ":\n 0|1"; 378 | $possiblevalues = ['0', '1']; 379 | } 380 | 381 | if ($default !== null & $default !== '') { 382 | $postfix .= "\n ".get_string('clidefault', 'tool_uploadusercli')." ".$default; 383 | } 384 | $options[$name] = [ 385 | 'hasvalue' => 'VALUE', 386 | 'description' => $label.$postfix, 387 | 'default' => $default, 388 | ]; 389 | if ($possiblevalues !== null) { 390 | $options[$name]['validation'] = function($v) use ($possiblevalues, $name) { 391 | if (!in_array('' . $v, $possiblevalues)) { 392 | $this->cli_error(get_string('clierrorargument', 'tool_uploadusercli', 393 | (object)['name' => $name, 'values' => join(', ', $possiblevalues)])); 394 | } 395 | }; 396 | } 397 | } 398 | return $options; 399 | } 400 | 401 | /** 402 | * Get process statistics. 403 | * 404 | * @return array 405 | */ 406 | public function get_stats(): array { 407 | return $this->process->get_stats(); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /locallib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Bulk user registration functions 19 | * 20 | * @package tool 21 | * @subpackage uploadusercli 22 | * @copyright 2004 onwards Martin Dougiamas (http://dougiamas.com) 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | define('UU_USER_ADDNEW', 0); 29 | define('UU_USER_ADDINC', 1); 30 | define('UU_USER_ADD_UPDATE', 2); 31 | define('UU_USER_UPDATE', 3); 32 | 33 | define('UU_UPDATE_NOCHANGES', 0); 34 | define('UU_UPDATE_FILEOVERRIDE', 1); 35 | define('UU_UPDATE_ALLOVERRIDE', 2); 36 | define('UU_UPDATE_MISSING', 3); 37 | 38 | define('UU_BULK_NONE', 0); 39 | define('UU_BULK_NEW', 1); 40 | define('UU_BULK_UPDATED', 2); 41 | define('UU_BULK_ALL', 3); 42 | 43 | define('UU_PWRESET_NONE', 0); 44 | define('UU_PWRESET_WEAK', 1); 45 | define('UU_PWRESET_ALL', 2); 46 | 47 | /** 48 | * Tracking of processed users. 49 | * 50 | * This class prints user information into a html table. 51 | * 52 | * @package core 53 | * @subpackage admin 54 | * @copyright 2007 Petr Skoda {@link http://skodak.org} 55 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 56 | */ 57 | class tool_uploadusercli_uu_progress_tracker { 58 | /** @var array */ 59 | protected $_row; 60 | 61 | /** 62 | * The columns shown on the table. 63 | * @var array 64 | */ 65 | public $columns = []; 66 | /** @var array column headers */ 67 | protected $headers = []; 68 | 69 | /** 70 | * tool_uploadusercli_uu_progress_tracker constructor. 71 | */ 72 | public function __construct() { 73 | $this->headers = [ 74 | 'status' => get_string('status'), 75 | 'line' => get_string('uucsvline', 'tool_uploadusercli'), 76 | 'id' => 'ID', 77 | 'username' => get_string('username'), 78 | 'firstname' => get_string('firstname'), 79 | 'lastname' => get_string('lastname'), 80 | 'email' => get_string('email'), 81 | 'password' => get_string('password'), 82 | 'auth' => get_string('authentication'), 83 | 'enrolments' => get_string('enrolments', 'enrol'), 84 | 'suspended' => get_string('suspended', 'auth'), 85 | 'theme' => get_string('theme'), 86 | 'deleted' => get_string('delete'), 87 | ]; 88 | $this->columns = array_keys($this->headers); 89 | } 90 | 91 | /** 92 | * Print table header. 93 | * @return void 94 | */ 95 | public function start() { 96 | $ci = 0; 97 | echo ''; 98 | echo ''; 99 | foreach ($this->headers as $key => $header) { 100 | echo ''; 101 | } 102 | echo ''; 103 | $this->_row = null; 104 | } 105 | 106 | /** 107 | * Flush previous line and start a new one. 108 | * @return void 109 | */ 110 | public function flush() { 111 | if (empty($this->_row) or empty($this->_row['line']['normal'])) { 112 | // Nothing to print - each line has to have at least number 113 | $this->_row = array(); 114 | foreach ($this->columns as $col) { 115 | $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>''); 116 | } 117 | return; 118 | } 119 | $ci = 0; 120 | $ri = 1; 121 | echo ''; 122 | foreach ($this->_row as $key=>$field) { 123 | foreach ($field as $type=>$content) { 124 | if ($field[$type] !== '') { 125 | $field[$type] = ''.$field[$type].''; 126 | } else { 127 | unset($field[$type]); 128 | } 129 | } 130 | echo ''; 137 | } 138 | echo ''; 139 | foreach ($this->columns as $col) { 140 | $this->_row[$col] = array('normal'=>'', 'info'=>'', 'warning'=>'', 'error'=>''); 141 | } 142 | } 143 | 144 | /** 145 | * Add tracking info 146 | * @param string $col name of column 147 | * @param string $msg message 148 | * @param string $level 'normal', 'warning' or 'error' 149 | * @param bool $merge true means add as new line, false means override all previous text of the same type 150 | * @return void 151 | */ 152 | public function track($col, $msg, $level = 'normal', $merge = true) { 153 | if (empty($this->_row)) { 154 | $this->flush(); //init arrays 155 | } 156 | if (!in_array($col, $this->columns)) { 157 | debugging('Incorrect column:'.$col); 158 | return; 159 | } 160 | if ($merge) { 161 | if ($this->_row[$col][$level] != '') { 162 | $this->_row[$col][$level] .='
'; 163 | } 164 | $this->_row[$col][$level] .= $msg; 165 | } else { 166 | $this->_row[$col][$level] = $msg; 167 | } 168 | } 169 | 170 | /** 171 | * Print the table end 172 | * @return void 173 | */ 174 | public function close() { 175 | $this->flush(); 176 | echo '
'.$header.'
'; 131 | if (!empty($field)) { 132 | echo implode('
', $field); 133 | } else { 134 | echo ' '; 135 | } 136 | echo '
'; 177 | } 178 | } 179 | 180 | /** 181 | * Validation callback function - verified the column line of csv file. 182 | * Converts standard column names to lowercase. 183 | * @param csv_import_reader $cir 184 | * @param array $stdfields standard user fields 185 | * @param array $profilefields custom profile fields 186 | * @param moodle_url $returnurl return url in case of any error 187 | * @return array list of fields 188 | */ 189 | function tool_uploadusercli_uu_validate_user_upload_columns(csv_import_reader $cir, $stdfields, $profilefields, moodle_url $returnurl) { 190 | $columns = $cir->get_columns(); 191 | 192 | if (empty($columns)) { 193 | $cir->close(); 194 | $cir->cleanup(); 195 | print_error('cannotreadtmpfile', 'error', $returnurl); 196 | } 197 | if (count($columns) < 2) { 198 | $cir->close(); 199 | $cir->cleanup(); 200 | print_error('csvfewcolumns', 'error', $returnurl); 201 | } 202 | 203 | // test columns 204 | $processed = array(); 205 | foreach ($columns as $key=>$unused) { 206 | $field = $columns[$key]; 207 | $field = trim($field); 208 | $lcfield = core_text::strtolower($field); 209 | if (in_array($field, $stdfields) or in_array($lcfield, $stdfields)) { 210 | // standard fields are only lowercase 211 | $newfield = $lcfield; 212 | 213 | } else if (in_array($field, $profilefields)) { 214 | // exact profile field name match - these are case sensitive 215 | $newfield = $field; 216 | 217 | } else if (in_array($lcfield, $profilefields)) { 218 | // hack: somebody wrote uppercase in csv file, but the system knows only lowercase profile field 219 | $newfield = $lcfield; 220 | 221 | } else if (preg_match('/^(sysrole|cohort|course|group|type|role|enrolperiod|enrolstatus|enroltimestart)\d+$/', $lcfield)) { 222 | // special fields for enrolments 223 | $newfield = $lcfield; 224 | 225 | } else { 226 | $cir->close(); 227 | $cir->cleanup(); 228 | print_error('invalidfieldname', 'error', $returnurl, $field); 229 | } 230 | if (in_array($newfield, $processed)) { 231 | $cir->close(); 232 | $cir->cleanup(); 233 | print_error('duplicatefieldname', 'error', $returnurl, $newfield); 234 | } 235 | $processed[$key] = $newfield; 236 | } 237 | 238 | return $processed; 239 | } 240 | 241 | /** 242 | * Increments username - increments trailing number or adds it if not present. 243 | * Varifies that the new username does not exist yet 244 | * @param string $username 245 | * @return incremented username which does not exist yet 246 | */ 247 | function tool_uploadusercli_uu_increment_username($username) { 248 | global $DB, $CFG; 249 | 250 | if (!preg_match_all('/(.*?)([0-9]+)$/', $username, $matches)) { 251 | $username = $username.'2'; 252 | } else { 253 | $username = $matches[1][0].($matches[2][0]+1); 254 | } 255 | 256 | if ($DB->record_exists('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { 257 | return tool_uploadusercli_uu_increment_username($username); 258 | } else { 259 | return $username; 260 | } 261 | } 262 | 263 | /** 264 | * Check if default field contains templates and apply them. 265 | * @param string template - potential tempalte string 266 | * @param object user object- we need username, firstname and lastname 267 | * @return string field value 268 | */ 269 | function tool_uploadusercli_uu_process_template($template, $user) { 270 | if (is_array($template)) { 271 | // hack for for support of text editors with format 272 | $t = $template['text']; 273 | } else { 274 | $t = $template; 275 | } 276 | if (strpos($t, '%') === false) { 277 | return $template; 278 | } 279 | 280 | $username = isset($user->username) ? $user->username : ''; 281 | $firstname = isset($user->firstname) ? $user->firstname : ''; 282 | $lastname = isset($user->lastname) ? $user->lastname : ''; 283 | 284 | $callback = partial('uu_process_template_callback', $username, $firstname, $lastname); 285 | 286 | $result = preg_replace_callback('/(?name 344 | */ 345 | function tool_uploadusercli_uu_supported_auths() { 346 | // Get all the enabled plugins. 347 | $plugins = get_enabled_auth_plugins(); 348 | $choices = array(); 349 | foreach ($plugins as $plugin) { 350 | $objplugin = get_auth_plugin($plugin); 351 | // If the plugin can not be manually set skip it. 352 | if (!$objplugin->can_be_manually_set()) { 353 | continue; 354 | } 355 | $choices[$plugin] = get_string('pluginname', "auth_{$plugin}"); 356 | } 357 | 358 | return $choices; 359 | } 360 | 361 | /** 362 | * Returns list of roles that are assignable in courses 363 | * @return array 364 | */ 365 | function tool_uploadusercli_uu_allowed_roles() { 366 | // let's cheat a bit, frontpage is guaranteed to exist and has the same list of roles ;-) 367 | $roles = get_assignable_roles(context_course::instance(SITEID), ROLENAME_ORIGINALANDSHORT); 368 | return array_reverse($roles, true); 369 | } 370 | 371 | /** 372 | * Returns mapping of all roles using short role name as index. 373 | * @return array 374 | */ 375 | function tool_uploadusercli_uu_allowed_roles_cache() { 376 | $allowedroles = get_assignable_roles(context_course::instance(SITEID), ROLENAME_SHORT); 377 | $rolecache = []; 378 | foreach ($allowedroles as $rid=>$rname) { 379 | $rolecache[$rid] = new stdClass(); 380 | $rolecache[$rid]->id = $rid; 381 | $rolecache[$rid]->name = $rname; 382 | if (!is_numeric($rname)) { // only non-numeric shortnames are supported!!! 383 | $rolecache[$rname] = new stdClass(); 384 | $rolecache[$rname]->id = $rid; 385 | $rolecache[$rname]->name = $rname; 386 | } 387 | } 388 | return $rolecache; 389 | } 390 | 391 | /** 392 | * Returns mapping of all system roles using short role name as index. 393 | * @return array 394 | */ 395 | function tool_uploadusercli_uu_allowed_sysroles_cache() { 396 | $allowedroles = get_assignable_roles(context_system::instance(), ROLENAME_SHORT); 397 | $rolecache = []; 398 | foreach ($allowedroles as $rid => $rname) { 399 | $rolecache[$rid] = new stdClass(); 400 | $rolecache[$rid]->id = $rid; 401 | $rolecache[$rid]->name = $rname; 402 | if (!is_numeric($rname)) { // Only non-numeric shortnames are supported! 403 | $rolecache[$rname] = new stdClass(); 404 | $rolecache[$rname]->id = $rid; 405 | $rolecache[$rname]->name = $rname; 406 | } 407 | } 408 | return $rolecache; 409 | } 410 | 411 | /** 412 | * Pre process custom profile data, and update it with corrected value 413 | * 414 | * @param stdClass $data user profile data 415 | * @return stdClass pre-processed custom profile data 416 | */ 417 | function tool_uploadusercli_uu_pre_process_custom_profile_data($data) { 418 | global $CFG, $DB; 419 | // find custom profile fields and check if data needs to converted. 420 | foreach ($data as $key => $value) { 421 | if (preg_match('/^profile_field_/', $key)) { 422 | $shortname = str_replace('profile_field_', '', $key); 423 | if ($fields = $DB->get_records('user_info_field', array('shortname' => $shortname))) { 424 | foreach ($fields as $field) { 425 | require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); 426 | $newfield = 'profile_field_'.$field->datatype; 427 | $formfield = new $newfield($field->id, $data->id); 428 | if (method_exists($formfield, 'convert_external_data')) { 429 | $data->$key = $formfield->convert_external_data($value); 430 | } 431 | } 432 | } 433 | } 434 | } 435 | return $data; 436 | } 437 | 438 | /** 439 | * Checks if data provided for custom fields is correct 440 | * Currently checking for custom profile field or type menu 441 | * 442 | * @param array $data user profile data 443 | * @return bool true if no error else false 444 | */ 445 | function tool_uploadusercli_uu_check_custom_profile_data(&$data) { 446 | global $CFG, $DB; 447 | $noerror = true; 448 | $testuserid = null; 449 | 450 | if (!empty($data['username'])) { 451 | if (preg_match('/id=(.*)"/i', $data['username'], $result)) { 452 | $testuserid = $result[1]; 453 | } 454 | } 455 | // Find custom profile fields and check if data needs to converted. 456 | foreach ($data as $key => $value) { 457 | if (preg_match('/^profile_field_/', $key)) { 458 | $shortname = str_replace('profile_field_', '', $key); 459 | if ($fields = $DB->get_records('user_info_field', array('shortname' => $shortname))) { 460 | foreach ($fields as $field) { 461 | require_once($CFG->dirroot.'/user/profile/field/'.$field->datatype.'/field.class.php'); 462 | $newfield = 'profile_field_'.$field->datatype; 463 | $formfield = new $newfield($field->id, 0); 464 | if (method_exists($formfield, 'convert_external_data') && 465 | is_null($formfield->convert_external_data($value))) { 466 | $data['status'][] = get_string('invaliduserfield', 'error', $shortname); 467 | $noerror = false; 468 | } 469 | // Check for duplicate value. 470 | if (method_exists($formfield, 'edit_validate_field') ) { 471 | $testuser = new stdClass(); 472 | $testuser->{$key} = $value; 473 | $testuser->id = $testuserid; 474 | $err = $formfield->edit_validate_field($testuser); 475 | if (!empty($err[$key])) { 476 | $data['status'][] = $err[$key].' ('.$key.')'; 477 | $noerror = false; 478 | } 479 | } 480 | } 481 | } 482 | } 483 | } 484 | return $noerror; 485 | } 486 | -------------------------------------------------------------------------------- /user_form.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Bulk user upload forms 19 | * 20 | * @package tool 21 | * @subpackage uploadusercli 22 | * @copyright 2007 Dan Poltawski 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | defined('MOODLE_INTERNAL') || die(); 27 | 28 | require_once $CFG->libdir.'/formslib.php'; 29 | require_once($CFG->dirroot . '/user/editlib.php'); 30 | 31 | /** 32 | * Upload a file CVS file with user information. 33 | * 34 | * @copyright 2007 Petr Skoda {@link http://skodak.org} 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class tool_uploadusercli_admin_uploaduser_form1 extends moodleform { 38 | function definition () { 39 | $mform = $this->_form; 40 | 41 | $mform->addElement('header', 'settingsheader', get_string('upload')); 42 | 43 | $url = new moodle_url('example.csv'); 44 | $link = html_writer::link($url, 'example.csv'); 45 | $mform->addElement('static', 'examplecsv', get_string('examplecsv', 'tool_uploadusercli'), $link); 46 | $mform->addHelpButton('examplecsv', 'examplecsv', 'tool_uploadusercli'); 47 | 48 | $mform->addElement('filepicker', 'userfile', get_string('file')); 49 | $mform->addRule('userfile', null, 'required'); 50 | 51 | $choices = csv_import_reader::get_delimiter_list(); 52 | $mform->addElement('select', 'delimiter_name', get_string('csvdelimiter', 'tool_uploadusercli'), $choices); 53 | if (array_key_exists('cfg', $choices)) { 54 | $mform->setDefault('delimiter_name', 'cfg'); 55 | } else if (get_string('listsep', 'langconfig') == ';') { 56 | $mform->setDefault('delimiter_name', 'semicolon'); 57 | } else { 58 | $mform->setDefault('delimiter_name', 'comma'); 59 | } 60 | 61 | $choices = core_text::get_encodings(); 62 | $mform->addElement('select', 'encoding', get_string('encoding', 'tool_uploadusercli'), $choices); 63 | $mform->setDefault('encoding', 'UTF-8'); 64 | 65 | $choices = array('10'=>10, '20'=>20, '100'=>100, '1000'=>1000, '100000'=>100000); 66 | $mform->addElement('select', 'previewrows', get_string('rowpreviewnum', 'tool_uploadusercli'), $choices); 67 | $mform->setType('previewrows', PARAM_INT); 68 | 69 | $this->add_action_buttons(false, get_string('uploadusers', 'tool_uploadusercli')); 70 | } 71 | 72 | /** 73 | * Returns list of elements and their default values, to be used in CLI 74 | * 75 | * @return array 76 | */ 77 | public function get_form_for_cli() { 78 | $elements = array_filter($this->_form->_elements, function($element) { 79 | return !in_array($element->getName(), ['buttonar', 'userfile', 'previewrows']); 80 | }); 81 | return [$elements, $this->_form->_defaultValues]; 82 | } 83 | } 84 | 85 | 86 | /** 87 | * Specify user upload details 88 | * 89 | * @copyright 2007 Petr Skoda {@link http://skodak.org} 90 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 91 | */ 92 | class tool_uploadusercli_admin_uploaduser_form2 extends moodleform { 93 | function definition () { 94 | global $CFG, $USER; 95 | 96 | $mform = $this->_form; 97 | $columns = $this->_customdata['columns']; 98 | $data = $this->_customdata['data']; 99 | 100 | // I am the template user, why should it be the administrator? we have roles now, other ppl may use this script ;-) 101 | $templateuser = $USER; 102 | 103 | // upload settings and file 104 | $mform->addElement('header', 'settingsheader', get_string('settings')); 105 | 106 | $choices = array(UU_USER_ADDNEW => get_string('uuoptype_addnew', 'tool_uploadusercli'), 107 | UU_USER_ADDINC => get_string('uuoptype_addinc', 'tool_uploadusercli'), 108 | UU_USER_ADD_UPDATE => get_string('uuoptype_addupdate', 'tool_uploadusercli'), 109 | UU_USER_UPDATE => get_string('uuoptype_update', 'tool_uploadusercli')); 110 | $mform->addElement('select', 'uutype', get_string('uuoptype', 'tool_uploadusercli'), $choices); 111 | 112 | $choices = array(0 => get_string('infilefield', 'auth'), 1 => get_string('createpasswordifneeded', 'auth')); 113 | $mform->addElement('select', 'uupasswordnew', get_string('uupasswordnew', 'tool_uploadusercli'), $choices); 114 | $mform->setDefault('uupasswordnew', 1); 115 | $mform->hideIf('uupasswordnew', 'uutype', 'eq', UU_USER_UPDATE); 116 | 117 | $choices = array(UU_UPDATE_NOCHANGES => get_string('nochanges', 'tool_uploadusercli'), 118 | UU_UPDATE_FILEOVERRIDE => get_string('uuupdatefromfile', 'tool_uploadusercli'), 119 | UU_UPDATE_ALLOVERRIDE => get_string('uuupdateall', 'tool_uploadusercli'), 120 | UU_UPDATE_MISSING => get_string('uuupdatemissing', 'tool_uploadusercli')); 121 | $mform->addElement('select', 'uuupdatetype', get_string('uuupdatetype', 'tool_uploadusercli'), $choices); 122 | $mform->setDefault('uuupdatetype', UU_UPDATE_NOCHANGES); 123 | $mform->hideIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDNEW); 124 | $mform->hideIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDINC); 125 | 126 | $choices = array(0 => get_string('nochanges', 'tool_uploadusercli'), 1 => get_string('update')); 127 | $mform->addElement('select', 'uupasswordold', get_string('uupasswordold', 'tool_uploadusercli'), $choices); 128 | $mform->setDefault('uupasswordold', 0); 129 | $mform->hideIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDNEW); 130 | $mform->hideIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDINC); 131 | $mform->hideIf('uupasswordold', 'uuupdatetype', 'eq', 0); 132 | $mform->hideIf('uupasswordold', 'uuupdatetype', 'eq', 3); 133 | 134 | $choices = array(UU_PWRESET_WEAK => get_string('usersweakpassword', 'tool_uploadusercli'), 135 | UU_PWRESET_NONE => get_string('none'), 136 | UU_PWRESET_ALL => get_string('all')); 137 | if (empty($CFG->passwordpolicy)) { 138 | unset($choices[UU_PWRESET_WEAK]); 139 | } 140 | $mform->addElement('select', 'uuforcepasswordchange', get_string('forcepasswordchange', 'core'), $choices); 141 | 142 | 143 | $mform->addElement('selectyesno', 'uuallowrenames', get_string('allowrenames', 'tool_uploadusercli')); 144 | $mform->setDefault('uuallowrenames', 0); 145 | $mform->hideIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDNEW); 146 | $mform->hideIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDINC); 147 | 148 | $mform->addElement('selectyesno', 'uuallowdeletes', get_string('allowdeletes', 'tool_uploadusercli')); 149 | $mform->setDefault('uuallowdeletes', 0); 150 | $mform->hideIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDNEW); 151 | $mform->hideIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDINC); 152 | 153 | $mform->addElement('selectyesno', 'uuallowsuspends', get_string('allowsuspends', 'tool_uploadusercli')); 154 | $mform->setDefault('uuallowsuspends', 1); 155 | $mform->hideIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW); 156 | $mform->hideIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC); 157 | 158 | if (!empty($CFG->allowaccountssameemail)) { 159 | $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploadusercli')); 160 | $mform->setDefault('uunoemailduplicates', 1); 161 | } else { 162 | $mform->addElement('hidden', 'uunoemailduplicates', 1); 163 | } 164 | $mform->setType('uunoemailduplicates', PARAM_BOOL); 165 | 166 | $mform->addElement('selectyesno', 'uustandardusernames', get_string('uustandardusernames', 'tool_uploadusercli')); 167 | $mform->setDefault('uustandardusernames', 1); 168 | 169 | $choices = array(UU_BULK_NONE => get_string('no'), 170 | UU_BULK_NEW => get_string('uubulknew', 'tool_uploadusercli'), 171 | UU_BULK_UPDATED => get_string('uubulkupdated', 'tool_uploadusercli'), 172 | UU_BULK_ALL => get_string('uubulkall', 'tool_uploadusercli')); 173 | $mform->addElement('select', 'uubulk', get_string('uubulk', 'tool_uploadusercli'), $choices); 174 | $mform->setDefault('uubulk', 0); 175 | 176 | // roles selection 177 | $showroles = false; 178 | foreach ($columns as $column) { 179 | if (preg_match('/^type\d+$/', $column)) { 180 | $showroles = true; 181 | break; 182 | } 183 | } 184 | if ($showroles) { 185 | $mform->addElement('header', 'rolesheader', get_string('roles')); 186 | 187 | $choices = tool_uploadusercli_uu_allowed_roles(true); 188 | 189 | $mform->addElement('select', 'uulegacy1', get_string('uulegacy1role', 'tool_uploadusercli'), $choices); 190 | if ($studentroles = get_archetype_roles('student')) { 191 | foreach ($studentroles as $role) { 192 | if (isset($choices[$role->id])) { 193 | $mform->setDefault('uulegacy1', $role->id); 194 | break; 195 | } 196 | } 197 | unset($studentroles); 198 | } 199 | 200 | $mform->addElement('select', 'uulegacy2', get_string('uulegacy2role', 'tool_uploadusercli'), $choices); 201 | if ($editteacherroles = get_archetype_roles('editingteacher')) { 202 | foreach ($editteacherroles as $role) { 203 | if (isset($choices[$role->id])) { 204 | $mform->setDefault('uulegacy2', $role->id); 205 | break; 206 | } 207 | } 208 | unset($editteacherroles); 209 | } 210 | 211 | $mform->addElement('select', 'uulegacy3', get_string('uulegacy3role', 'tool_uploadusercli'), $choices); 212 | if ($teacherroles = get_archetype_roles('teacher')) { 213 | foreach ($teacherroles as $role) { 214 | if (isset($choices[$role->id])) { 215 | $mform->setDefault('uulegacy3', $role->id); 216 | break; 217 | } 218 | } 219 | unset($teacherroles); 220 | } 221 | } 222 | 223 | // default values 224 | $mform->addElement('header', 'defaultheader', get_string('defaultvalues', 'tool_uploadusercli')); 225 | 226 | $mform->addElement('text', 'username', get_string('uuusernametemplate', 'tool_uploadusercli'), 'size="20"'); 227 | $mform->setType('username', PARAM_RAW); // No cleaning here. The process verifies it later. 228 | $mform->addRule('username', get_string('requiredtemplate', 'tool_uploadusercli'), 'required', null, 'client'); 229 | $mform->hideIf('username', 'uutype', 'eq', UU_USER_ADD_UPDATE); 230 | $mform->hideIf('username', 'uutype', 'eq', UU_USER_UPDATE); 231 | $mform->setForceLtr('username'); 232 | 233 | $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"'); 234 | $mform->setType('email', PARAM_RAW); // No cleaning here. The process verifies it later. 235 | $mform->hideIf('email', 'uutype', 'eq', UU_USER_ADD_UPDATE); 236 | $mform->hideIf('email', 'uutype', 'eq', UU_USER_UPDATE); 237 | $mform->setForceLtr('email'); 238 | 239 | // only enabled and known to work plugins 240 | $choices = tool_uploadusercli_uu_supported_auths(); 241 | $mform->addElement('select', 'auth', get_string('chooseauthmethod','auth'), $choices); 242 | $mform->setDefault('auth', 'manual'); // manual is a sensible backwards compatible default 243 | $mform->addHelpButton('auth', 'chooseauthmethod', 'auth'); 244 | $mform->setAdvanced('auth'); 245 | 246 | $choices = array(0 => get_string('emaildisplayno'), 1 => get_string('emaildisplayyes'), 2 => get_string('emaildisplaycourse')); 247 | $mform->addElement('select', 'maildisplay', get_string('emaildisplay'), $choices); 248 | $mform->setDefault('maildisplay', core_user::get_property_default('maildisplay')); 249 | $mform->addHelpButton('maildisplay', 'emaildisplay'); 250 | 251 | $choices = array(0 => get_string('emailenable'), 1 => get_string('emaildisable')); 252 | $mform->addElement('select', 'emailstop', get_string('emailstop'), $choices); 253 | $mform->setDefault('emailstop', core_user::get_property_default('emailstop')); 254 | $mform->setAdvanced('emailstop'); 255 | 256 | $choices = array(0 => get_string('textformat'), 1 => get_string('htmlformat')); 257 | $mform->addElement('select', 'mailformat', get_string('emailformat'), $choices); 258 | $mform->setDefault('mailformat', core_user::get_property_default('mailformat')); 259 | $mform->setAdvanced('mailformat'); 260 | 261 | $choices = array(0 => get_string('emaildigestoff'), 1 => get_string('emaildigestcomplete'), 2 => get_string('emaildigestsubjects')); 262 | $mform->addElement('select', 'maildigest', get_string('emaildigest'), $choices); 263 | $mform->setDefault('maildigest', core_user::get_property_default('maildigest')); 264 | $mform->setAdvanced('maildigest'); 265 | 266 | $choices = array(1 => get_string('autosubscribeyes'), 0 => get_string('autosubscribeno')); 267 | $mform->addElement('select', 'autosubscribe', get_string('autosubscribe'), $choices); 268 | $mform->setDefault('autosubscribe', core_user::get_property_default('autosubscribe')); 269 | 270 | $mform->addElement('text', 'city', get_string('city'), 'maxlength="120" size="25"'); 271 | $mform->setType('city', PARAM_TEXT); 272 | if (empty($CFG->defaultcity)) { 273 | $mform->setDefault('city', $templateuser->city); 274 | } else { 275 | $mform->setDefault('city', core_user::get_property_default('city')); 276 | } 277 | 278 | $choices = get_string_manager()->get_list_of_countries(); 279 | $choices = array(''=>get_string('selectacountry').'...') + $choices; 280 | $mform->addElement('select', 'country', get_string('selectacountry'), $choices); 281 | if (empty($CFG->country)) { 282 | $mform->setDefault('country', $templateuser->country); 283 | } else { 284 | $mform->setDefault('country', core_user::get_property_default('country')); 285 | } 286 | $mform->setAdvanced('country'); 287 | 288 | $choices = core_date::get_list_of_timezones($templateuser->timezone, true); 289 | $mform->addElement('select', 'timezone', get_string('timezone'), $choices); 290 | $mform->setDefault('timezone', $templateuser->timezone); 291 | $mform->setAdvanced('timezone'); 292 | 293 | $mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations()); 294 | $mform->setDefault('lang', $templateuser->lang); 295 | $mform->setAdvanced('lang'); 296 | 297 | $editoroptions = array('maxfiles'=>0, 'maxbytes'=>0, 'trusttext'=>false, 'forcehttps'=>false); 298 | $mform->addElement('editor', 'description', get_string('userdescription'), null, $editoroptions); 299 | $mform->setType('description', PARAM_CLEANHTML); 300 | $mform->addHelpButton('description', 'userdescription'); 301 | $mform->setAdvanced('description'); 302 | 303 | $mform->addElement('text', 'url', get_string('webpage'), 'maxlength="255" size="50"'); 304 | $mform->setType('url', PARAM_URL); 305 | $mform->setAdvanced('url'); 306 | 307 | $mform->addElement('text', 'idnumber', get_string('idnumber'), 'maxlength="255" size="25"'); 308 | $mform->setType('idnumber', PARAM_NOTAGS); 309 | $mform->setForceLtr('idnumber'); 310 | 311 | $mform->addElement('text', 'institution', get_string('institution'), 'maxlength="255" size="25"'); 312 | $mform->setType('institution', PARAM_TEXT); 313 | $mform->setDefault('institution', $templateuser->institution); 314 | 315 | $mform->addElement('text', 'department', get_string('department'), 'maxlength="255" size="25"'); 316 | $mform->setType('department', PARAM_TEXT); 317 | $mform->setDefault('department', $templateuser->department); 318 | 319 | $mform->addElement('text', 'phone1', get_string('phone1'), 'maxlength="20" size="25"'); 320 | $mform->setType('phone1', PARAM_NOTAGS); 321 | $mform->setAdvanced('phone1'); 322 | $mform->setForceLtr('phone1'); 323 | 324 | $mform->addElement('text', 'phone2', get_string('phone2'), 'maxlength="20" size="25"'); 325 | $mform->setType('phone2', PARAM_NOTAGS); 326 | $mform->setAdvanced('phone2'); 327 | $mform->setForceLtr('phone2'); 328 | 329 | $mform->addElement('text', 'address', get_string('address'), 'maxlength="255" size="25"'); 330 | $mform->setType('address', PARAM_TEXT); 331 | $mform->setAdvanced('address'); 332 | 333 | // Next the profile defaults 334 | profile_definition($mform); 335 | 336 | // hidden fields 337 | $mform->addElement('hidden', 'iid'); 338 | $mform->setType('iid', PARAM_INT); 339 | 340 | $mform->addElement('hidden', 'previewrows'); 341 | $mform->setType('previewrows', PARAM_INT); 342 | 343 | $this->add_action_buttons(true, get_string('uploadusers', 'tool_uploadusercli')); 344 | 345 | $this->set_data($data); 346 | } 347 | 348 | /** 349 | * Form tweaks that depend on current data. 350 | */ 351 | function definition_after_data() { 352 | $mform = $this->_form; 353 | $columns = $this->_customdata['columns']; 354 | 355 | foreach ($columns as $column) { 356 | if ($mform->elementExists($column)) { 357 | $mform->removeElement($column); 358 | } 359 | } 360 | 361 | if (!in_array('password', $columns)) { 362 | // password resetting makes sense only if password specified in csv file 363 | if ($mform->elementExists('uuforcepasswordchange')) { 364 | $mform->removeElement('uuforcepasswordchange'); 365 | } 366 | } 367 | } 368 | 369 | /** 370 | * Server side validation. 371 | */ 372 | function validation($data, $files) { 373 | $errors = parent::validation($data, $files); 374 | $columns = $this->_customdata['columns']; 375 | $optype = $data['uutype']; 376 | $updatetype = $data['uuupdatetype']; 377 | 378 | // detect if password column needed in file 379 | if (!in_array('password', $columns)) { 380 | switch ($optype) { 381 | case UU_USER_UPDATE: 382 | if (!empty($data['uupasswordold'])) { 383 | $errors['uupasswordold'] = get_string('missingfield', 'error', 'password'); 384 | } 385 | break; 386 | 387 | case UU_USER_ADD_UPDATE: 388 | if (empty($data['uupasswordnew'])) { 389 | $errors['uupasswordnew'] = get_string('missingfield', 'error', 'password'); 390 | } 391 | if (!empty($data['uupasswordold'])) { 392 | $errors['uupasswordold'] = get_string('missingfield', 'error', 'password'); 393 | } 394 | break; 395 | 396 | case UU_USER_ADDNEW: 397 | if (empty($data['uupasswordnew'])) { 398 | $errors['uupasswordnew'] = get_string('missingfield', 'error', 'password'); 399 | } 400 | break; 401 | case UU_USER_ADDINC: 402 | if (empty($data['uupasswordnew'])) { 403 | $errors['uupasswordnew'] = get_string('missingfield', 'error', 'password'); 404 | } 405 | break; 406 | } 407 | } 408 | 409 | // If the 'Existing user details' value is set we need to ensure that the 410 | // 'Upload type' is not set to something invalid. 411 | if (!empty($updatetype) && ($optype == UU_USER_ADDNEW || $optype == UU_USER_ADDINC)) { 412 | $errors['uuupdatetype'] = get_string('invalidupdatetype', 'tool_uploadusercli'); 413 | } 414 | 415 | // look for other required data 416 | if ($optype != UU_USER_UPDATE) { 417 | $requiredusernames = useredit_get_required_name_fields(); 418 | $missing = array(); 419 | foreach ($requiredusernames as $requiredusername) { 420 | if (!in_array($requiredusername, $columns)) { 421 | $missing[] = get_string('missingfield', 'error', $requiredusername);; 422 | } 423 | } 424 | if ($missing) { 425 | $errors['uutype'] = implode('
', $missing); 426 | } 427 | if (!in_array('email', $columns) and empty($data['email'])) { 428 | $errors['email'] = get_string('requiredtemplate', 'tool_uploadusercli'); 429 | } 430 | } 431 | return $errors; 432 | } 433 | 434 | /** 435 | * Used to reformat the data from the editor component 436 | * 437 | * @return stdClass 438 | */ 439 | function get_data() { 440 | $data = parent::get_data(); 441 | 442 | if ($data !== null and isset($data->description)) { 443 | $data->descriptionformat = $data->description['format']; 444 | $data->description = $data->description['text']; 445 | } 446 | 447 | return $data; 448 | } 449 | 450 | /** 451 | * Returns list of elements and their default values, to be used in CLI 452 | * 453 | * @return array 454 | */ 455 | public function get_form_for_cli() { 456 | $elements = array_filter($this->_form->_elements, function($element) { 457 | return !in_array($element->getName(), ['buttonar', 'uubulk']); 458 | }); 459 | return [$elements, $this->_form->_defaultValues]; 460 | } 461 | 462 | /** 463 | * Returns validation errors (used in CLI) 464 | * 465 | * @return array 466 | */ 467 | public function get_validation_errors(): array { 468 | return $this->_form->_errors; 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /classes/process.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Class process 19 | * 20 | * @package tool_uploadusercli 21 | * @copyright 2020 Moodle 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace tool_uploadusercli; 26 | 27 | defined('MOODLE_INTERNAL') || die(); 28 | 29 | use tool_uploadusercli\local\field_value_validators; 30 | 31 | require_once($CFG->dirroot.'/user/profile/lib.php'); 32 | require_once($CFG->dirroot.'/user/lib.php'); 33 | require_once($CFG->dirroot.'/group/lib.php'); 34 | require_once($CFG->dirroot.'/cohort/lib.php'); 35 | require_once($CFG->libdir.'/csvlib.class.php'); 36 | require_once($CFG->dirroot.'/'.$CFG->admin.'/tool/uploadusercli/locallib.php'); 37 | 38 | /** 39 | * Process CSV file with users data, this will create/update users, enrol them into courses, etc 40 | * 41 | * @package tool_uploadusercli 42 | * @copyright 2020 Moodle 43 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 44 | */ 45 | class process { 46 | 47 | /** @var \csv_import_reader */ 48 | protected $cir; 49 | /** @var \stdClass */ 50 | protected $formdata; 51 | /** @var \tool_uploadusercli_uu_progress_tracker */ 52 | protected $upt; 53 | /** @var array */ 54 | protected $filecolumns = null; 55 | /** @var int */ 56 | protected $today; 57 | /** @var \enrol_plugin|null */ 58 | protected $manualenrol = null; 59 | /** @var array */ 60 | protected $standardfields = []; 61 | /** @var array */ 62 | protected $profilefields = []; 63 | /** @var array */ 64 | protected $allprofilefields = []; 65 | /** @var string|\tool_uploadusercli_uu_progress_tracker|null */ 66 | protected $progresstrackerclass = null; 67 | 68 | /** @var int */ 69 | protected $usersnew = 0; 70 | /** @var int */ 71 | protected $usersupdated = 0; 72 | /** @var int /not printed yet anywhere */ 73 | protected $usersuptodate = 0; 74 | /** @var int */ 75 | protected $userserrors = 0; 76 | /** @var int */ 77 | protected $deletes = 0; 78 | /** @var int */ 79 | protected $deleteerrors = 0; 80 | /** @var int */ 81 | protected $renames = 0; 82 | /** @var int */ 83 | protected $renameerrors = 0; 84 | /** @var int */ 85 | protected $usersskipped = 0; 86 | /** @var int */ 87 | protected $weakpasswords = 0; 88 | 89 | /** @var array course cache - do not fetch all courses here, we will not probably use them all anyway */ 90 | protected $ccache = []; 91 | /** @var array */ 92 | protected $cohorts = []; 93 | /** @var array Course roles lookup cache. */ 94 | protected $rolecache = []; 95 | /** @var array System roles lookup cache. */ 96 | protected $sysrolecache = []; 97 | /** @var array cache of used manual enrol plugins in each course */ 98 | protected $manualcache = []; 99 | /** @var array officially supported plugins that are enabled */ 100 | protected $supportedauths = []; 101 | 102 | /** 103 | * process constructor. 104 | * 105 | * @param \csv_import_reader $cir 106 | * @param string|null $progresstrackerclass 107 | * @throws \coding_exception 108 | */ 109 | public function __construct(\csv_import_reader $cir, string $progresstrackerclass = null) { 110 | $this->cir = $cir; 111 | if ($progresstrackerclass) { 112 | if (!class_exists($progresstrackerclass) || !is_subclass_of($progresstrackerclass, \tool_uploadusercli_uu_progress_tracker::class)) { 113 | throw new \coding_exception('Progress tracker class must extend \tool_uploadusercli_uu_progress_tracker'); 114 | } 115 | $this->progresstrackerclass = $progresstrackerclass; 116 | } else { 117 | $this->progresstrackerclass = \tool_uploadusercli_uu_progress_tracker::class; 118 | } 119 | 120 | // Keep timestamp consistent. 121 | $today = time(); 122 | $today = make_timestamp(date('Y', $today), date('m', $today), date('d', $today), 0, 0, 0); 123 | $this->today = $today; 124 | 125 | $this->rolecache = tool_uploadusercli_uu_allowed_roles_cache(); // Course roles lookup cache. 126 | $this->sysrolecache = tool_uploadusercli_uu_allowed_sysroles_cache(); // System roles lookup cache. 127 | $this->supportedauths = tool_uploadusercli_uu_supported_auths(); // Officially supported plugins that are enabled. 128 | 129 | if (enrol_is_enabled('manual')) { 130 | // We use only manual enrol plugin here, if it is disabled no enrol is done. 131 | $this->manualenrol = enrol_get_plugin('manual'); 132 | } 133 | 134 | $this->find_profile_fields(); 135 | $this->find_standard_fields(); 136 | } 137 | 138 | /** 139 | * Standard user fields. 140 | */ 141 | protected function find_standard_fields(): void { 142 | $this->standardfields = array('id', 'username', 'email', 'emailstop', 143 | 'city', 'country', 'lang', 'timezone', 'mailformat', 144 | 'maildisplay', 'maildigest', 'htmleditor', 'autosubscribe', 145 | 'institution', 'department', 'idnumber', 'skype', 146 | 'msn', 'aim', 'yahoo', 'icq', 'phone1', 'phone2', 'address', 147 | 'url', '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, get_all_user_name_fields()); 159 | } 160 | 161 | /** 162 | * Profile fields 163 | */ 164 | protected function find_profile_fields(): void { 165 | global $DB; 166 | $this->allprofilefields = $DB->get_records('user_info_field'); 167 | $this->profilefields = []; 168 | if ($proffields = $this->allprofilefields) { 169 | foreach ($proffields as $key => $proffield) { 170 | $profilefieldname = 'profile_field_'.$proffield->shortname; 171 | $this->profilefields[] = $profilefieldname; 172 | // Re-index $proffields with key as shortname. This will be 173 | // used while checking if profile data is key and needs to be converted (eg. menu profile field). 174 | $proffields[$profilefieldname] = $proffield; 175 | unset($proffields[$key]); 176 | } 177 | $this->allprofilefields = $proffields; 178 | } 179 | } 180 | 181 | /** 182 | * Returns the list of columns in the file 183 | * 184 | * @return array 185 | */ 186 | public function get_file_columns(): array { 187 | if ($this->filecolumns === null) { 188 | $returnurl = new \moodle_url('/admin/tool/uploadusercli/index.php'); 189 | $this->filecolumns = tool_uploadusercli_uu_validate_user_upload_columns($this->cir, 190 | $this->standardfields, $this->profilefields, $returnurl); 191 | } 192 | return $this->filecolumns; 193 | } 194 | 195 | /** 196 | * Set data from the form (or from CLI options) 197 | * 198 | * @param \stdClass $formdata 199 | */ 200 | public function set_form_data(\stdClass $formdata): void { 201 | global $SESSION; 202 | $this->formdata = $formdata; 203 | 204 | // Clear bulk selection. 205 | if ($this->get_bulk()) { 206 | $SESSION->bulk_users = array(); 207 | } 208 | } 209 | 210 | /** 211 | * Operation type 212 | * @return int 213 | */ 214 | protected function get_operation_type(): int { 215 | return (int)$this->formdata->uutype; 216 | } 217 | 218 | /** 219 | * Setting to allow deletes 220 | * @return bool 221 | */ 222 | protected function get_allow_deletes(): bool { 223 | $optype = $this->get_operation_type(); 224 | return (!empty($this->formdata->uuallowdeletes) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC); 225 | } 226 | 227 | /** 228 | * Setting to allow deletes 229 | * @return bool 230 | */ 231 | protected function get_allow_renames(): bool { 232 | $optype = $this->get_operation_type(); 233 | return (!empty($this->formdata->uuallowrenames) and $optype != UU_USER_ADDNEW and $optype != UU_USER_ADDINC); 234 | } 235 | 236 | /** 237 | * Setting to select for bulk actions (not available in CLI) 238 | * @return bool 239 | */ 240 | public function get_bulk(): bool { 241 | return $this->formdata->uubulk ?? false; 242 | } 243 | 244 | /** 245 | * Setting for update type 246 | * @return int 247 | */ 248 | protected function get_update_type(): int { 249 | return isset($this->formdata->uuupdatetype) ? $this->formdata->uuupdatetype : 0; 250 | } 251 | 252 | /** 253 | * Setting to allow update passwords 254 | * @return bool 255 | */ 256 | protected function get_update_passwords(): bool { 257 | return !empty($this->formdata->uupasswordold) 258 | and $this->get_operation_type() != UU_USER_ADDNEW 259 | and $this->get_operation_type() != UU_USER_ADDINC 260 | and ($this->get_update_type() == UU_UPDATE_FILEOVERRIDE or $this->get_update_type() == UU_UPDATE_ALLOVERRIDE); 261 | } 262 | 263 | /** 264 | * Setting to allow email duplicates 265 | * @return bool 266 | */ 267 | protected function get_allow_email_duplicates(): bool { 268 | global $CFG; 269 | return !(empty($CFG->allowaccountssameemail) ? 1 : $this->formdata->uunoemailduplicates); 270 | } 271 | 272 | /** 273 | * Setting for reset password 274 | * @return int UU_PWRESET_NONE, UU_PWRESET_WEAK, UU_PWRESET_ALL 275 | */ 276 | protected function get_reset_passwords(): int { 277 | return isset($this->formdata->uuforcepasswordchange) ? $this->formdata->uuforcepasswordchange : UU_PWRESET_NONE; 278 | } 279 | 280 | /** 281 | * Setting to allow create passwords 282 | * @return bool 283 | */ 284 | protected function get_create_paswords(): bool { 285 | return (!empty($this->formdata->uupasswordnew) and $this->get_operation_type() != UU_USER_UPDATE); 286 | } 287 | 288 | /** 289 | * Setting to allow suspends 290 | * @return bool 291 | */ 292 | protected function get_allow_suspends(): bool { 293 | return !empty($this->formdata->uuallowsuspends); 294 | } 295 | 296 | /** 297 | * Setting to normalise user names 298 | * @return bool 299 | */ 300 | protected function get_normalise_user_names(): bool { 301 | return !empty($this->formdata->uustandardusernames); 302 | } 303 | 304 | /** 305 | * Helper method to return Yes/No string 306 | * 307 | * @param bool $value 308 | * @return string 309 | */ 310 | protected function get_string_yes_no($value): string { 311 | return $value ? get_string('yes') : get_string('no'); 312 | } 313 | 314 | /** 315 | * Process the CSV file 316 | */ 317 | public function process() { 318 | // Init csv import helper. 319 | $this->cir->init(); 320 | 321 | $classname = $this->progresstrackerclass; 322 | $this->upt = new $classname(); 323 | $this->upt->start(); // Start table. 324 | 325 | $linenum = 1; // Column header is first line. 326 | while ($line = $this->cir->next()) { 327 | $this->upt->flush(); 328 | $linenum++; 329 | 330 | $this->upt->track('line', $linenum); 331 | $this->process_line($line); 332 | } 333 | 334 | $this->upt->close(); // Close table. 335 | $this->cir->close(); 336 | $this->cir->cleanup(true); 337 | } 338 | 339 | /** 340 | * Prepare one line from CSV file as a user record 341 | * 342 | * @param array $line 343 | * @return \stdClass|null 344 | */ 345 | protected function prepare_user_record(array $line): ?\stdClass { 346 | global $CFG, $USER; 347 | 348 | $user = new \stdClass(); 349 | 350 | // Add fields to user object. 351 | foreach ($line as $keynum => $value) { 352 | if (!isset($this->get_file_columns()[$keynum])) { 353 | // This should not happen. 354 | continue; 355 | } 356 | $key = $this->get_file_columns()[$keynum]; 357 | if (strpos($key, 'profile_field_') === 0) { 358 | // NOTE: bloody mega hack alert!! 359 | if (isset($USER->$key) and is_array($USER->$key)) { 360 | // This must be some hacky field that is abusing arrays to store content and format. 361 | $user->$key = array(); 362 | $user->{$key['text']} = $value; 363 | $user->{$key['format']} = FORMAT_MOODLE; 364 | } else { 365 | $user->$key = trim($value); 366 | } 367 | } else { 368 | $user->$key = trim($value); 369 | } 370 | 371 | if (in_array($key, $this->upt->columns)) { 372 | // Default value in progress tracking table, can be changed later. 373 | $this->upt->track($key, s($value), 'normal'); 374 | } 375 | } 376 | if (!isset($user->username)) { 377 | // Prevent warnings below. 378 | $user->username = ''; 379 | } 380 | 381 | if ($this->get_operation_type() == UU_USER_ADDNEW or $this->get_operation_type() == UU_USER_ADDINC) { 382 | // User creation is a special case - the username may be constructed from templates using firstname and lastname 383 | // better never try this in mixed update types. 384 | $error = false; 385 | if (!isset($user->firstname) or $user->firstname === '') { 386 | $this->upt->track('status', get_string('missingfield', 'error', 'firstname'), 'error'); 387 | $this->upt->track('firstname', get_string('error'), 'error'); 388 | $error = true; 389 | } 390 | if (!isset($user->lastname) or $user->lastname === '') { 391 | $this->upt->track('status', get_string('missingfield', 'error', 'lastname'), 'error'); 392 | $this->upt->track('lastname', get_string('error'), 'error'); 393 | $error = true; 394 | } 395 | if ($error) { 396 | $this->userserrors++; 397 | return null; 398 | } 399 | // We require username too - we might use template for it though. 400 | if (empty($user->username) and !empty($this->formdata->username)) { 401 | $user->username = tool_uploadusercli_uu_process_template($this->formdata->username, $user); 402 | $this->upt->track('username', s($user->username)); 403 | } 404 | } 405 | 406 | // Normalize username. 407 | $user->originalusername = $user->username; 408 | if ($this->get_normalise_user_names()) { 409 | $user->username = \core_user::clean_field($user->username, 'username'); 410 | } 411 | 412 | // Make sure we really have username. 413 | if (empty($user->username)) { 414 | $this->upt->track('status', get_string('missingfield', 'error', 'username'), 'error'); 415 | $this->upt->track('username', get_string('error'), 'error'); 416 | $this->userserrors++; 417 | return null; 418 | } else if ($user->username === 'guest') { 419 | $this->upt->track('status', get_string('guestnoeditprofileother', 'error'), 'error'); 420 | $this->userserrors++; 421 | return null; 422 | } 423 | 424 | if ($user->username !== \core_user::clean_field($user->username, 'username')) { 425 | $this->upt->track('status', get_string('invalidusername', 'error', 'username'), 'error'); 426 | $this->upt->track('username', get_string('error'), 'error'); 427 | $this->userserrors++; 428 | } 429 | 430 | if (empty($user->mnethostid)) { 431 | $user->mnethostid = $CFG->mnet_localhost_id; 432 | } 433 | 434 | return $user; 435 | } 436 | 437 | /** 438 | * Process one line from CSV file 439 | * 440 | * @param array $line 441 | * @throws \coding_exception 442 | * @throws \dml_exception 443 | * @throws \moodle_exception 444 | */ 445 | public function process_line(array $line) { 446 | global $DB, $CFG, $SESSION; 447 | 448 | if (!$user = $this->prepare_user_record($line)) { 449 | return; 450 | } 451 | 452 | if ($existinguser = $DB->get_record('user', ['username' => $user->username, 'mnethostid' => $user->mnethostid])) { 453 | $this->upt->track('id', $existinguser->id, 'normal', false); 454 | } 455 | 456 | if ($user->mnethostid == $CFG->mnet_localhost_id) { 457 | $remoteuser = false; 458 | 459 | // Find out if username incrementing required. 460 | if ($existinguser and $this->get_operation_type() == UU_USER_ADDINC) { 461 | $user->username = tool_uploadusercli_uu_increment_username($user->username); 462 | $existinguser = false; 463 | } 464 | 465 | } else { 466 | if (!$existinguser or $this->get_operation_type() == UU_USER_ADDINC) { 467 | $this->upt->track('status', get_string('errormnetadd', 'tool_uploadusercli'), 'error'); 468 | $this->userserrors++; 469 | return; 470 | } 471 | 472 | $remoteuser = true; 473 | 474 | // Make sure there are no changes of existing fields except the suspended status. 475 | foreach ((array)$existinguser as $k => $v) { 476 | if ($k === 'suspended') { 477 | continue; 478 | } 479 | if (property_exists($user, $k)) { 480 | $user->$k = $v; 481 | } 482 | if (in_array($k, $this->upt->columns)) { 483 | if ($k === 'password' or $k === 'oldusername' or $k === 'deleted') { 484 | $this->upt->track($k, '', 'normal', false); 485 | } else { 486 | $this->upt->track($k, s($v), 'normal', false); 487 | } 488 | } 489 | } 490 | unset($user->oldusername); 491 | unset($user->password); 492 | $user->auth = $existinguser->auth; 493 | } 494 | 495 | // Notify about nay username changes. 496 | if ($user->originalusername !== $user->username) { 497 | $this->upt->track('username', '', 'normal', false); // Clear previous. 498 | $this->upt->track('username', s($user->originalusername).'-->'.s($user->username), 'info'); 499 | } else { 500 | $this->upt->track('username', s($user->username), 'normal', false); 501 | } 502 | unset($user->originalusername); 503 | 504 | // Verify if the theme is valid and allowed to be set. 505 | if (isset($user->theme)) { 506 | list($status, $message) = field_value_validators::validate_theme($user->theme); 507 | if ($status !== 'normal' && !empty($message)) { 508 | $this->upt->track('status', $message, $status); 509 | // Unset the theme when validation fails. 510 | unset($user->theme); 511 | } 512 | } 513 | 514 | // Add default values for remaining fields. 515 | $formdefaults = array(); 516 | if (!$existinguser || 517 | ($this->get_update_type() != UU_UPDATE_FILEOVERRIDE && $this->get_update_type() != UU_UPDATE_NOCHANGES)) { 518 | foreach ($this->standardfields as $field) { 519 | if (isset($user->$field)) { 520 | continue; 521 | } 522 | // All validation moved to form2. 523 | if (isset($this->formdata->$field)) { 524 | // Process templates. 525 | $user->$field = tool_uploadusercli_uu_process_template($this->formdata->$field, $user); 526 | $formdefaults[$field] = true; 527 | if (in_array($field, $this->upt->columns)) { 528 | $this->upt->track($field, s($user->$field), 'normal'); 529 | } 530 | } 531 | } 532 | $proffields = $this->allprofilefields; 533 | foreach ($this->profilefields as $field) { 534 | if (isset($user->$field)) { 535 | continue; 536 | } 537 | if (isset($this->formdata->$field)) { 538 | // Process templates. 539 | $user->$field = tool_uploadusercli_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 | require_once($CFG->dirroot.'/user/profile/field/'.$proffields[$field]->datatype.'/field.class.php'); 544 | $profilefieldclass = 'profile_field_'.$proffields[$field]->datatype; 545 | $profilefield = new $profilefieldclass($proffields[$field]->id); 546 | if (method_exists($profilefield, 'convert_external_data')) { 547 | $user->$field = $profilefield->edit_save_data_preprocess($user->$field, null); 548 | } 549 | 550 | $formdefaults[$field] = true; 551 | } 552 | } 553 | } 554 | 555 | // Delete user. 556 | if (!empty($user->deleted)) { 557 | if (!$this->get_allow_deletes() or $remoteuser) { 558 | $this->usersskipped++; 559 | $this->upt->track('status', get_string('usernotdeletedoff', 'error'), 'warning'); 560 | return; 561 | } 562 | if ($existinguser) { 563 | if (is_siteadmin($existinguser->id)) { 564 | $this->upt->track('status', get_string('usernotdeletedadmin', 'error'), 'error'); 565 | $this->deleteerrors++; 566 | return; 567 | } 568 | if (delete_user($existinguser)) { 569 | $this->upt->track('status', get_string('userdeleted', 'tool_uploadusercli')); 570 | $this->deletes++; 571 | } else { 572 | $this->upt->track('status', get_string('usernotdeletederror', 'error'), 'error'); 573 | $this->deleteerrors++; 574 | } 575 | } else { 576 | $this->upt->track('status', get_string('usernotdeletedmissing', 'error'), 'error'); 577 | $this->deleteerrors++; 578 | } 579 | return; 580 | } 581 | // We do not need the deleted flag anymore. 582 | unset($user->deleted); 583 | 584 | // Renaming requested? 585 | if (!empty($user->oldusername) ) { 586 | if (!$this->get_allow_renames()) { 587 | $this->usersskipped++; 588 | $this->upt->track('status', get_string('usernotrenamedoff', 'error'), 'warning'); 589 | return; 590 | } 591 | 592 | if ($existinguser) { 593 | $this->upt->track('status', get_string('usernotrenamedexists', 'error'), 'error'); 594 | $this->renameerrors++; 595 | return; 596 | } 597 | 598 | if ($user->username === 'guest') { 599 | $this->upt->track('status', get_string('guestnoeditprofileother', 'error'), 'error'); 600 | $this->renameerrors++; 601 | return; 602 | } 603 | 604 | if ($this->get_normalise_user_names()) { 605 | $oldusername = \core_user::clean_field($user->oldusername, 'username'); 606 | } else { 607 | $oldusername = $user->oldusername; 608 | } 609 | 610 | // No guessing when looking for old username, it must be exact match. 611 | if ($olduser = $DB->get_record('user', 612 | ['username' => $oldusername, 'mnethostid' => $CFG->mnet_localhost_id])) { 613 | $this->upt->track('id', $olduser->id, 'normal', false); 614 | if (is_siteadmin($olduser->id)) { 615 | $this->upt->track('status', get_string('usernotrenamedadmin', 'error'), 'error'); 616 | $this->renameerrors++; 617 | return; 618 | } 619 | $DB->set_field('user', 'username', $user->username, ['id' => $olduser->id]); 620 | $this->upt->track('username', '', 'normal', false); // Clear previous. 621 | $this->upt->track('username', s($oldusername).'-->'.s($user->username), 'info'); 622 | $this->upt->track('status', get_string('userrenamed', 'tool_uploadusercli')); 623 | $this->renames++; 624 | } else { 625 | $this->upt->track('status', get_string('usernotrenamedmissing', 'error'), 'error'); 626 | $this->renameerrors++; 627 | return; 628 | } 629 | $existinguser = $olduser; 630 | $existinguser->username = $user->username; 631 | } 632 | 633 | // Can we process with update or insert? 634 | $skip = false; 635 | switch ($this->get_operation_type()) { 636 | case UU_USER_ADDNEW: 637 | if ($existinguser) { 638 | $this->usersskipped++; 639 | $this->upt->track('status', get_string('usernotaddedregistered', 'error'), 'warning'); 640 | $skip = true; 641 | } 642 | break; 643 | 644 | case UU_USER_ADDINC: 645 | if ($existinguser) { 646 | // This should not happen! 647 | $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); 648 | $this->userserrors++; 649 | $skip = true; 650 | } 651 | break; 652 | 653 | case UU_USER_ADD_UPDATE: 654 | break; 655 | 656 | case UU_USER_UPDATE: 657 | if (!$existinguser) { 658 | $this->usersskipped++; 659 | $this->upt->track('status', get_string('usernotupdatednotexists', 'error'), 'warning'); 660 | $skip = true; 661 | } 662 | break; 663 | 664 | default: 665 | // Unknown type. 666 | $skip = true; 667 | } 668 | 669 | if ($skip) { 670 | return; 671 | } 672 | 673 | if ($existinguser) { 674 | $user->id = $existinguser->id; 675 | 676 | $this->upt->track('username', \html_writer::link( 677 | new \moodle_url('/user/profile.php', ['id' => $existinguser->id]), s($existinguser->username)), 'normal', false); 678 | $this->upt->track('suspended', $this->get_string_yes_no($existinguser->suspended) , 'normal', false); 679 | $this->upt->track('auth', $existinguser->auth, 'normal', false); 680 | 681 | if (is_siteadmin($user->id)) { 682 | $this->upt->track('status', get_string('usernotupdatedadmin', 'error'), 'error'); 683 | $this->userserrors++; 684 | return; 685 | } 686 | 687 | $existinguser->timemodified = time(); 688 | // Do NOT mess with timecreated or firstaccess here! 689 | 690 | // Load existing profile data. 691 | profile_load_data($existinguser); 692 | 693 | $doupdate = false; 694 | $dologout = false; 695 | 696 | if ($this->get_update_type() != UU_UPDATE_NOCHANGES and !$remoteuser) { 697 | if (!empty($user->auth) and $user->auth !== $existinguser->auth) { 698 | $this->upt->track('auth', s($existinguser->auth).'-->'.s($user->auth), 'info', false); 699 | $existinguser->auth = $user->auth; 700 | if (!isset($this->supportedauths[$user->auth])) { 701 | $this->upt->track('auth', get_string('userauthunsupported', 'error'), 'warning'); 702 | } 703 | $doupdate = true; 704 | if ($existinguser->auth === 'nologin') { 705 | $dologout = true; 706 | } 707 | } 708 | $allcolumns = array_merge($this->standardfields, $this->profilefields); 709 | foreach ($allcolumns as $column) { 710 | if ($column === 'username' or $column === 'password' or $column === 'auth' or $column === 'suspended') { 711 | // These can not be changed here. 712 | continue; 713 | } 714 | if (!property_exists($user, $column) or !property_exists($existinguser, $column)) { 715 | continue; 716 | } 717 | if ($this->get_update_type() == UU_UPDATE_MISSING) { 718 | if (!is_null($existinguser->$column) and $existinguser->$column !== '') { 719 | continue; 720 | } 721 | } else if ($this->get_update_type() == UU_UPDATE_ALLOVERRIDE) { 722 | // We override everything. 723 | null; 724 | } else if ($this->get_update_type() == UU_UPDATE_FILEOVERRIDE) { 725 | if (!empty($formdefaults[$column])) { 726 | // Do not override with form defaults. 727 | continue; 728 | } 729 | } 730 | if ($existinguser->$column !== $user->$column) { 731 | if ($column === 'email') { 732 | $select = $DB->sql_like('email', ':email', false, true, false, '|'); 733 | $params = array('email' => $DB->sql_like_escape($user->email, '|')); 734 | if ($DB->record_exists_select('user', $select , $params)) { 735 | 736 | $changeincase = \core_text::strtolower($existinguser->$column) === \core_text::strtolower( 737 | $user->$column); 738 | 739 | if ($changeincase) { 740 | // If only case is different then switch to lower case and carry on. 741 | $user->$column = \core_text::strtolower($user->$column); 742 | continue; 743 | } else if (!$this->get_allow_email_duplicates()) { 744 | $this->upt->track('email', get_string('useremailduplicate', 'error'), 'error'); 745 | $this->upt->track('status', get_string('usernotupdatederror', 'error'), 'error'); 746 | $this->userserrors++; 747 | return; 748 | } else { 749 | $this->upt->track('email', get_string('useremailduplicate', 'error'), 'warning'); 750 | } 751 | } 752 | if (!validate_email($user->email)) { 753 | $this->upt->track('email', get_string('invalidemail'), 'warning'); 754 | } 755 | } 756 | 757 | if ($column === 'lang') { 758 | if (empty($user->lang)) { 759 | // Do not change to not-set value. 760 | continue; 761 | } else if (\core_user::clean_field($user->lang, 'lang') === '') { 762 | $this->upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning'); 763 | continue; 764 | } 765 | } 766 | 767 | if (in_array($column, $this->upt->columns)) { 768 | $this->upt->track($column, s($existinguser->$column).'-->'.s($user->$column), 'info', false); 769 | } 770 | $existinguser->$column = $user->$column; 771 | $doupdate = true; 772 | } 773 | } 774 | } 775 | 776 | try { 777 | $auth = get_auth_plugin($existinguser->auth); 778 | } catch (\Exception $e) { 779 | $this->upt->track('auth', get_string('userautherror', 'error', s($existinguser->auth)), 'error'); 780 | $this->upt->track('status', get_string('usernotupdatederror', 'error'), 'error'); 781 | $this->userserrors++; 782 | return; 783 | } 784 | $isinternalauth = $auth->is_internal(); 785 | 786 | // Deal with suspending and activating of accounts. 787 | if ($this->get_allow_suspends() and isset($user->suspended) and $user->suspended !== '') { 788 | $user->suspended = $user->suspended ? 1 : 0; 789 | if ($existinguser->suspended != $user->suspended) { 790 | $this->upt->track('suspended', '', 'normal', false); 791 | $this->upt->track('suspended', 792 | $this->get_string_yes_no($existinguser->suspended).'-->'.$this->get_string_yes_no($user->suspended), 793 | 'info', false); 794 | $existinguser->suspended = $user->suspended; 795 | $doupdate = true; 796 | if ($existinguser->suspended) { 797 | $dologout = true; 798 | } 799 | } 800 | } 801 | 802 | // Changing of passwords is a special case 803 | // do not force password changes for external auth plugins! 804 | $oldpw = $existinguser->password; 805 | 806 | if ($remoteuser) { 807 | // Do not mess with passwords of remote users. 808 | null; 809 | } else if (!$isinternalauth) { 810 | $existinguser->password = AUTH_PASSWORD_NOT_CACHED; 811 | $this->upt->track('password', '-', 'normal', false); 812 | // Clean up prefs. 813 | unset_user_preference('create_password', $existinguser); 814 | unset_user_preference('auth_forcepasswordchange', $existinguser); 815 | 816 | } else if (!empty($user->password)) { 817 | if ($this->get_update_passwords()) { 818 | // Check for passwords that we want to force users to reset next 819 | // time they log in. 820 | $errmsg = null; 821 | $weak = !check_password_policy($user->password, $errmsg, $user); 822 | if ($this->get_reset_passwords() == UU_PWRESET_ALL or 823 | ($this->get_reset_passwords() == UU_PWRESET_WEAK and $weak)) { 824 | if ($weak) { 825 | $this->weakpasswords++; 826 | $this->upt->track('password', get_string('invalidpasswordpolicy', 'error'), 'warning'); 827 | } 828 | set_user_preference('auth_forcepasswordchange', 1, $existinguser); 829 | } else { 830 | unset_user_preference('auth_forcepasswordchange', $existinguser); 831 | } 832 | unset_user_preference('create_password', $existinguser); // No need to create password any more. 833 | 834 | // Use a low cost factor when generating bcrypt hash otherwise 835 | // hashing would be slow when uploading lots of users. Hashes 836 | // will be automatically updated to a higher cost factor the first 837 | // time the user logs in. 838 | $existinguser->password = hash_internal_user_password($user->password, true); 839 | $this->upt->track('password', $user->password, 'normal', false); 840 | } else { 841 | // Do not print password when not changed. 842 | $this->upt->track('password', '', 'normal', false); 843 | } 844 | } 845 | 846 | if ($doupdate or $existinguser->password !== $oldpw) { 847 | // We want only users that were really updated. 848 | user_update_user($existinguser, false, false); 849 | 850 | $this->upt->track('status', get_string('useraccountupdated', 'tool_uploadusercli')); 851 | $this->usersupdated++; 852 | 853 | if (!$remoteuser) { 854 | // Pre-process custom profile menu fields data from csv file. 855 | $existinguser = tool_uploadusercli_uu_pre_process_custom_profile_data($existinguser); 856 | // Save custom profile fields data from csv file. 857 | profile_save_data($existinguser); 858 | } 859 | 860 | if ($this->get_bulk() == UU_BULK_UPDATED or $this->get_bulk() == UU_BULK_ALL) { 861 | if (!in_array($user->id, $SESSION->bulk_users)) { 862 | $SESSION->bulk_users[] = $user->id; 863 | } 864 | } 865 | 866 | // Trigger event. 867 | \core\event\user_updated::create_from_userid($existinguser->id)->trigger(); 868 | 869 | } else { 870 | // No user information changed. 871 | $this->upt->track('status', get_string('useraccountuptodate', 'tool_uploadusercli')); 872 | $this->usersuptodate++; 873 | 874 | if ($this->get_bulk() == UU_BULK_ALL) { 875 | if (!in_array($user->id, $SESSION->bulk_users)) { 876 | $SESSION->bulk_users[] = $user->id; 877 | } 878 | } 879 | } 880 | 881 | if ($dologout) { 882 | \core\session\manager::kill_user_sessions($existinguser->id); 883 | } 884 | 885 | } else { 886 | // Save the new user to the database. 887 | $user->confirmed = 1; 888 | $user->timemodified = time(); 889 | $user->timecreated = time(); 890 | $user->mnethostid = $CFG->mnet_localhost_id; // We support ONLY local accounts here, sorry. 891 | 892 | if (!isset($user->suspended) or $user->suspended === '') { 893 | $user->suspended = 0; 894 | } else { 895 | $user->suspended = $user->suspended ? 1 : 0; 896 | } 897 | $this->upt->track('suspended', $this->get_string_yes_no($user->suspended), 'normal', false); 898 | 899 | if (empty($user->auth)) { 900 | $user->auth = 'manual'; 901 | } 902 | $this->upt->track('auth', $user->auth, 'normal', false); 903 | 904 | // Do not insert record if new auth plugin does not exist! 905 | try { 906 | $auth = get_auth_plugin($user->auth); 907 | } catch (\Exception $e) { 908 | $this->upt->track('auth', get_string('userautherror', 'error', s($user->auth)), 'error'); 909 | $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); 910 | $this->userserrors++; 911 | return; 912 | } 913 | if (!isset($this->supportedauths[$user->auth])) { 914 | $this->upt->track('auth', get_string('userauthunsupported', 'error'), 'warning'); 915 | } 916 | 917 | $isinternalauth = $auth->is_internal(); 918 | 919 | if (empty($user->email)) { 920 | $this->upt->track('email', get_string('invalidemail'), 'error'); 921 | $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); 922 | $this->userserrors++; 923 | return; 924 | 925 | } else if ($DB->record_exists('user', ['email' => $user->email])) { 926 | if (!$this->get_allow_email_duplicates()) { 927 | $this->upt->track('email', get_string('useremailduplicate', 'error'), 'error'); 928 | $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); 929 | $this->userserrors++; 930 | return; 931 | } else { 932 | $this->upt->track('email', get_string('useremailduplicate', 'error'), 'warning'); 933 | } 934 | } 935 | if (!validate_email($user->email)) { 936 | $this->upt->track('email', get_string('invalidemail'), 'warning'); 937 | } 938 | 939 | if (empty($user->lang)) { 940 | $user->lang = ''; 941 | } else if (\core_user::clean_field($user->lang, 'lang') === '') { 942 | $this->upt->track('status', get_string('cannotfindlang', 'error', $user->lang), 'warning'); 943 | $user->lang = ''; 944 | } 945 | 946 | $forcechangepassword = false; 947 | 948 | if ($isinternalauth) { 949 | if (empty($user->password)) { 950 | if ($this->get_create_paswords()) { 951 | $user->password = 'to be generated'; 952 | $this->upt->track('password', '', 'normal', false); 953 | $this->upt->track('password', get_string('uupasswordcron', 'tool_uploadusercli'), 'warning', false); 954 | } else { 955 | $this->upt->track('password', '', 'normal', false); 956 | $this->upt->track('password', get_string('missingfield', 'error', 'password'), 'error'); 957 | $this->upt->track('status', get_string('usernotaddederror', 'error'), 'error'); 958 | $this->userserrors++; 959 | return; 960 | } 961 | } else { 962 | $errmsg = null; 963 | $weak = !check_password_policy($user->password, $errmsg, $user); 964 | if ($this->get_reset_passwords() == UU_PWRESET_ALL or 965 | ($this->get_reset_passwords() == UU_PWRESET_WEAK and $weak)) { 966 | if ($weak) { 967 | $this->weakpasswords++; 968 | $this->upt->track('password', get_string('invalidpasswordpolicy', 'error'), 'warning'); 969 | } 970 | $forcechangepassword = true; 971 | } 972 | // Use a low cost factor when generating bcrypt hash otherwise 973 | // hashing would be slow when uploading lots of users. Hashes 974 | // will be automatically updated to a higher cost factor the first 975 | // time the user logs in. 976 | $user->password = hash_internal_user_password($user->password, true); 977 | } 978 | } else { 979 | $user->password = AUTH_PASSWORD_NOT_CACHED; 980 | $this->upt->track('password', '-', 'normal', false); 981 | } 982 | 983 | $user->id = user_create_user($user, false, false); 984 | $this->upt->track('username', \html_writer::link( 985 | new \moodle_url('/user/profile.php', ['id' => $user->id]), s($user->username)), 'normal', false); 986 | 987 | // Pre-process custom profile menu fields data from csv file. 988 | $user = tool_uploadusercli_uu_pre_process_custom_profile_data($user); 989 | // Save custom profile fields data. 990 | profile_save_data($user); 991 | 992 | if ($forcechangepassword) { 993 | set_user_preference('auth_forcepasswordchange', 1, $user); 994 | } 995 | if ($user->password === 'to be generated') { 996 | set_user_preference('create_password', 1, $user); 997 | } 998 | 999 | // Trigger event. 1000 | \core\event\user_created::create_from_userid($user->id)->trigger(); 1001 | 1002 | $this->upt->track('status', get_string('newuser')); 1003 | $this->upt->track('id', $user->id, 'normal', false); 1004 | $this->usersnew++; 1005 | 1006 | // Make sure user context exists. 1007 | \context_user::instance($user->id); 1008 | 1009 | if ($this->get_bulk() == UU_BULK_NEW or $this->get_bulk() == UU_BULK_ALL) { 1010 | if (!in_array($user->id, $SESSION->bulk_users)) { 1011 | $SESSION->bulk_users[] = $user->id; 1012 | } 1013 | } 1014 | } 1015 | 1016 | // Update user interests. 1017 | if (isset($user->interests) && strval($user->interests) !== '') { 1018 | useredit_update_interests($user, preg_split('/\s*,\s*/', $user->interests, -1, PREG_SPLIT_NO_EMPTY)); 1019 | } 1020 | 1021 | // Add to cohort first, it might trigger enrolments indirectly - do NOT create cohorts here! 1022 | foreach ($this->get_file_columns() as $column) { 1023 | if (!preg_match('/^cohort\d+$/', $column)) { 1024 | continue; 1025 | } 1026 | 1027 | if (!empty($user->$column)) { 1028 | $addcohort = $user->$column; 1029 | if (!isset($this->cohorts[$addcohort])) { 1030 | if (is_number($addcohort)) { 1031 | // Only non-numeric idnumbers! 1032 | $cohort = $DB->get_record('cohort', ['id' => $addcohort]); 1033 | } else { 1034 | $cohort = $DB->get_record('cohort', ['idnumber' => $addcohort]); 1035 | if (empty($cohort) && has_capability('moodle/cohort:manage', \context_system::instance())) { 1036 | // Cohort was not found. Create a new one. 1037 | $cohortid = cohort_add_cohort((object)array( 1038 | 'idnumber' => $addcohort, 1039 | 'name' => $addcohort, 1040 | 'contextid' => \context_system::instance()->id 1041 | )); 1042 | $cohort = $DB->get_record('cohort', ['id' => $cohortid]); 1043 | } 1044 | } 1045 | 1046 | if (empty($cohort)) { 1047 | $this->cohorts[$addcohort] = get_string('unknowncohort', 'core_cohort', s($addcohort)); 1048 | } else if (!empty($cohort->component)) { 1049 | // Cohorts synchronised with external sources must not be modified! 1050 | $this->cohorts[$addcohort] = get_string('external', 'core_cohort'); 1051 | } else { 1052 | $this->cohorts[$addcohort] = $cohort; 1053 | } 1054 | } 1055 | 1056 | if (is_object($this->cohorts[$addcohort])) { 1057 | $cohort = $this->cohorts[$addcohort]; 1058 | if (!$DB->record_exists('cohort_members', ['cohortid' => $cohort->id, 'userid' => $user->id])) { 1059 | cohort_add_member($cohort->id, $user->id); 1060 | // We might add special column later, for now let's abuse enrolments. 1061 | $this->upt->track('enrolments', get_string('useradded', 'core_cohort', s($cohort->name)), 'info'); 1062 | } 1063 | } else { 1064 | // Error message. 1065 | $this->upt->track('enrolments', $this->cohorts[$addcohort], 'error'); 1066 | } 1067 | } 1068 | } 1069 | 1070 | // Find course enrolments, groups, roles/types and enrol periods 1071 | // this is again a special case, we always do this for any updated or created users. 1072 | foreach ($this->get_file_columns() as $column) { 1073 | if (preg_match('/^sysrole\d+$/', $column)) { 1074 | 1075 | if (!empty($user->$column)) { 1076 | $sysrolename = $user->$column; 1077 | if ($sysrolename[0] == '-') { 1078 | $removing = true; 1079 | $sysrolename = substr($sysrolename, 1); 1080 | } else { 1081 | $removing = false; 1082 | } 1083 | 1084 | if (array_key_exists($sysrolename, $this->sysrolecache)) { 1085 | $sysroleid = $this->sysrolecache[$sysrolename]->id; 1086 | } else { 1087 | $this->upt->track('enrolments', get_string('unknownrole', 'error', s($sysrolename)), 'error'); 1088 | continue; 1089 | } 1090 | 1091 | if ($removing) { 1092 | if (user_has_role_assignment($user->id, $sysroleid, SYSCONTEXTID)) { 1093 | role_unassign($sysroleid, $user->id, SYSCONTEXTID); 1094 | $this->upt->track('enrolments', get_string('unassignedsysrole', 1095 | 'tool_uploadusercli', $this->sysrolecache[$sysroleid]->name), 'info'); 1096 | } 1097 | } else { 1098 | if (!user_has_role_assignment($user->id, $sysroleid, SYSCONTEXTID)) { 1099 | role_assign($sysroleid, $user->id, SYSCONTEXTID); 1100 | $this->upt->track('enrolments', get_string('assignedsysrole', 1101 | 'tool_uploadusercli', $this->sysrolecache[$sysroleid]->name), 'info'); 1102 | } 1103 | } 1104 | } 1105 | 1106 | continue; 1107 | } 1108 | if (!preg_match('/^course\d+$/', $column)) { 1109 | continue; 1110 | } 1111 | $i = substr($column, 6); 1112 | 1113 | if (empty($user->{'course'.$i})) { 1114 | continue; 1115 | } 1116 | $shortname = $user->{'course'.$i}; 1117 | if (!array_key_exists($shortname, $this->ccache)) { 1118 | if (!$course = $DB->get_record('course', ['shortname' => $shortname], 'id, shortname')) { 1119 | $this->upt->track('enrolments', get_string('unknowncourse', 'error', s($shortname)), 'error'); 1120 | continue; 1121 | } 1122 | $this->ccache[$shortname] = $course; 1123 | $this->ccache[$shortname]->groups = null; 1124 | } 1125 | $courseid = $this->ccache[$shortname]->id; 1126 | $coursecontext = \context_course::instance($courseid); 1127 | if (!isset($this->manualcache[$courseid])) { 1128 | $this->manualcache[$courseid] = false; 1129 | if ($this->manualenrol) { 1130 | if ($instances = enrol_get_instances($courseid, false)) { 1131 | foreach ($instances as $instance) { 1132 | if ($instance->enrol === 'manual') { 1133 | $this->manualcache[$courseid] = $instance; 1134 | break; 1135 | } 1136 | } 1137 | } 1138 | } 1139 | } 1140 | 1141 | if ($courseid == SITEID) { 1142 | // Technically frontpage does not have enrolments, but only role assignments, 1143 | // let's not invent new lang strings here for this rarely used feature. 1144 | 1145 | if (!empty($user->{'role'.$i})) { 1146 | $rolename = $user->{'role'.$i}; 1147 | if (array_key_exists($rolename, $this->rolecache)) { 1148 | $roleid = $this->rolecache[$rolename]->id; 1149 | } else { 1150 | $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error'); 1151 | continue; 1152 | } 1153 | 1154 | role_assign($roleid, $user->id, \context_course::instance($courseid)); 1155 | 1156 | $a = new \stdClass(); 1157 | $a->course = $shortname; 1158 | $a->role = $this->rolecache[$roleid]->name; 1159 | $this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info'); 1160 | } 1161 | 1162 | } else if ($this->manualenrol and $this->manualcache[$courseid]) { 1163 | 1164 | // Find role. 1165 | $roleid = false; 1166 | if (!empty($user->{'role'.$i})) { 1167 | $rolename = $user->{'role'.$i}; 1168 | if (array_key_exists($rolename, $this->rolecache)) { 1169 | $roleid = $this->rolecache[$rolename]->id; 1170 | } else { 1171 | $this->upt->track('enrolments', get_string('unknownrole', 'error', s($rolename)), 'error'); 1172 | continue; 1173 | } 1174 | 1175 | } else if (!empty($user->{'type'.$i})) { 1176 | // If no role, then find "old" enrolment type. 1177 | $addtype = $user->{'type'.$i}; 1178 | if ($addtype < 1 or $addtype > 3) { 1179 | $this->upt->track('enrolments', get_string('error').': typeN = 1|2|3', 'error'); 1180 | continue; 1181 | } else if (empty($this->formdata->{'uulegacy'.$addtype})) { 1182 | continue; 1183 | } else { 1184 | $roleid = $this->formdata->{'uulegacy'.$addtype}; 1185 | } 1186 | } else { 1187 | // No role specified, use the default from manual enrol plugin. 1188 | $roleid = $this->manualcache[$courseid]->roleid; 1189 | } 1190 | 1191 | if ($roleid) { 1192 | // Find duration and/or enrol status. 1193 | $timeend = 0; 1194 | $timestart = $this->today; 1195 | $status = null; 1196 | 1197 | if (isset($user->{'enrolstatus'.$i})) { 1198 | $enrolstatus = $user->{'enrolstatus'.$i}; 1199 | if ($enrolstatus == '') { 1200 | $status = null; 1201 | } else if ($enrolstatus === (string)ENROL_USER_ACTIVE) { 1202 | $status = ENROL_USER_ACTIVE; 1203 | } else if ($enrolstatus === (string)ENROL_USER_SUSPENDED) { 1204 | $status = ENROL_USER_SUSPENDED; 1205 | } else { 1206 | debugging('Unknown enrolment status.'); 1207 | } 1208 | } 1209 | 1210 | if (!empty($user->{'enroltimestart'.$i})) { 1211 | $parsedtimestart = strtotime($user->{'enroltimestart'.$i}); 1212 | if ($parsedtimestart !== false) { 1213 | $timestart = $parsedtimestart; 1214 | } 1215 | } 1216 | 1217 | if (!empty($user->{'enrolperiod'.$i})) { 1218 | $duration = (int)$user->{'enrolperiod'.$i} * 60 * 60 * 24; // Convert days to seconds. 1219 | if ($duration > 0) { // Sanity check. 1220 | $timeend = $timestart + $duration; 1221 | } 1222 | } else if ($this->manualcache[$courseid]->enrolperiod > 0) { 1223 | $timeend = $timestart + $this->manualcache[$courseid]->enrolperiod; 1224 | } 1225 | 1226 | $this->manualenrol->enrol_user($this->manualcache[$courseid], $user->id, $roleid, 1227 | $timestart, $timeend, $status); 1228 | 1229 | $a = new \stdClass(); 1230 | $a->course = $shortname; 1231 | $a->role = $this->rolecache[$roleid]->name; 1232 | $this->upt->track('enrolments', get_string('enrolledincourserole', 'enrol_manual', $a), 'info'); 1233 | } 1234 | } 1235 | 1236 | // Find group to add to. 1237 | if (!empty($user->{'group'.$i})) { 1238 | // Make sure user is enrolled into course before adding into groups. 1239 | if (!is_enrolled($coursecontext, $user->id)) { 1240 | $this->upt->track('enrolments', get_string('addedtogroupnotenrolled', '', $user->{'group'.$i}), 'error'); 1241 | continue; 1242 | } 1243 | // Build group cache. 1244 | if (is_null($this->ccache[$shortname]->groups)) { 1245 | $this->ccache[$shortname]->groups = array(); 1246 | if ($groups = groups_get_all_groups($courseid)) { 1247 | foreach ($groups as $gid => $group) { 1248 | $this->ccache[$shortname]->groups[$gid] = new \stdClass(); 1249 | $this->ccache[$shortname]->groups[$gid]->id = $gid; 1250 | $this->ccache[$shortname]->groups[$gid]->name = $group->name; 1251 | if (!is_numeric($group->name)) { // Only non-numeric names are supported!!! 1252 | $this->ccache[$shortname]->groups[$group->name] = new \stdClass(); 1253 | $this->ccache[$shortname]->groups[$group->name]->id = $gid; 1254 | $this->ccache[$shortname]->groups[$group->name]->name = $group->name; 1255 | } 1256 | } 1257 | } 1258 | } 1259 | // Group exists? 1260 | $addgroup = $user->{'group'.$i}; 1261 | if (!array_key_exists($addgroup, $this->ccache[$shortname]->groups)) { 1262 | // If group doesn't exist, create it. 1263 | $newgroupdata = new \stdClass(); 1264 | $newgroupdata->name = $addgroup; 1265 | $newgroupdata->courseid = $this->ccache[$shortname]->id; 1266 | $newgroupdata->description = ''; 1267 | $gid = groups_create_group($newgroupdata); 1268 | if ($gid) { 1269 | $this->ccache[$shortname]->groups[$addgroup] = new \stdClass(); 1270 | $this->ccache[$shortname]->groups[$addgroup]->id = $gid; 1271 | $this->ccache[$shortname]->groups[$addgroup]->name = $newgroupdata->name; 1272 | } else { 1273 | $this->upt->track('enrolments', get_string('unknowngroup', 'error', s($addgroup)), 'error'); 1274 | continue; 1275 | } 1276 | } 1277 | $gid = $this->ccache[$shortname]->groups[$addgroup]->id; 1278 | $gname = $this->ccache[$shortname]->groups[$addgroup]->name; 1279 | 1280 | try { 1281 | if (groups_add_member($gid, $user->id)) { 1282 | $this->upt->track('enrolments', get_string('addedtogroup', '', s($gname)), 'info'); 1283 | } else { 1284 | $this->upt->track('enrolments', get_string('addedtogroupnot', '', s($gname)), 'error'); 1285 | } 1286 | } catch (\moodle_exception $e) { 1287 | $this->upt->track('enrolments', get_string('addedtogroupnot', '', s($gname)), 'error'); 1288 | continue; 1289 | } 1290 | } 1291 | } 1292 | if (($invalid = \core_user::validate($user)) !== true) { 1293 | $this->upt->track('status', get_string('invaliduserdata', 'tool_uploadusercli', s($user->username)), 'warning'); 1294 | } 1295 | } 1296 | 1297 | /** 1298 | * Summary about the whole process (how many users created, skipped, updated, etc) 1299 | * 1300 | * @return array 1301 | */ 1302 | public function get_stats() { 1303 | $lines = []; 1304 | 1305 | if ($this->get_operation_type() != UU_USER_UPDATE) { 1306 | $lines[] = get_string('userscreated', 'tool_uploadusercli').': '.$this->usersnew; 1307 | } 1308 | if ($this->get_operation_type() == UU_USER_UPDATE or $this->get_operation_type() == UU_USER_ADD_UPDATE) { 1309 | $lines[] = get_string('usersupdated', 'tool_uploadusercli').': '.$this->usersupdated; 1310 | } 1311 | if ($this->get_allow_deletes()) { 1312 | $lines[] = get_string('usersdeleted', 'tool_uploadusercli').': '.$this->deletes; 1313 | $lines[] = get_string('deleteerrors', 'tool_uploadusercli').': '.$this->deleteerrors; 1314 | } 1315 | if ($this->get_allow_renames()) { 1316 | $lines[] = get_string('usersrenamed', 'tool_uploadusercli').': '.$this->renames; 1317 | $lines[] = get_string('renameerrors', 'tool_uploadusercli').': '.$this->renameerrors; 1318 | } 1319 | if ($usersskipped = $this->usersskipped) { 1320 | $lines[] = get_string('usersskipped', 'tool_uploadusercli').': '.$usersskipped; 1321 | } 1322 | $lines[] = get_string('usersweakpassword', 'tool_uploadusercli').': '.$this->weakpasswords; 1323 | $lines[] = get_string('errors', 'tool_uploadusercli').': '.$this->userserrors; 1324 | 1325 | return $lines; 1326 | } 1327 | } 1328 | --------------------------------------------------------------------------------