Commit 2f632f79 authored by Richard Mansfield's avatar Richard Mansfield
Browse files

Allow updates to existing users by csv upload



Adds an 'Update Users' checkbox to the CSV upload page.  When checked,
users on the site whose usernames appear in the CSV file will have their
details overwritten by the values in the CSV file.

To be updated, existing users must be in the same institution as the one
specified in the upload form.

Change-Id: Ia47f7034adbade21dd26f455c356f58ed7bbf730
Signed-off-by: default avatarRichard Mansfield <richard.mansfield@catalyst.net.nz>
parent 74e5f9e1
......@@ -70,6 +70,8 @@ $ALLOWEDKEYS = array(
'industry',
'authinstance'
);
$UPDATES = array(); // During validation, remember which users already exist
$INSTITUTIONNAME = array(); // Mapping from institution id to display name
if ($USER->get('admin')) {
$authinstances = auth_get_auth_instances();
......@@ -88,6 +90,7 @@ if (count($authinstances) > 0) {
foreach ($authinstances as $authinstance) {
if ($USER->can_edit_institution($authinstance->name)) {
$options[$authinstance->id] = $authinstance->displayname. ': '.$authinstance->instancename;
$INSTITUTIONNAME[$authinstance->name] = $authinstance->displayname;
}
}
if ($USER->get('admin')) {
......@@ -138,6 +141,12 @@ $form = array(
'description' => get_string('emailusersaboutnewaccountdescription', 'admin'),
'defaultvalue' => true,
),
'updateusers' => array(
'type' => 'checkbox',
'title' => get_string('updateusers', 'admin'),
'description' => get_string('updateusersdescription', 'admin'),
'defaultvalue' => false,
),
'submit' => array(
'type' => 'submit',
'value' => get_string('uploadcsv', 'admin')
......@@ -164,7 +173,7 @@ if (!($USER->get('admin') || get_config_plugin('artefact', 'file', 'institutiona
* @param array $values The values submitted
*/
function uploadcsv_validate(Pieform $form, $values) {
global $CSVDATA, $ALLOWEDKEYS, $FORMAT, $USER;
global $CSVDATA, $ALLOWEDKEYS, $FORMAT, $USER, $INSTITUTIONNAME, $UPDATES;
// Don't even start attempting to parse if there are previous errors
if ($form->has_errors()) {
......@@ -193,9 +202,6 @@ function uploadcsv_validate(Pieform $form, $values) {
$authobj = AuthFactory::create($authinstance);
$usernames = array();
$emails = array();
$csvusers = new CsvFile($values['file']['tmp_name']);
$csvusers->set('allowedkeys', $ALLOWEDKEYS);
......@@ -218,6 +224,11 @@ function uploadcsv_validate(Pieform $form, $values) {
return;
}
// First pass validates usernames & passwords in the file, and builds
// up a list indexed by username.
$emails = array();
foreach ($csvdata->data as $key => $line) {
// If headers exists, increment i = key + 2 for actual line number
$i = ($csvusers->get('headerExists')) ? ($key + 2) : ($key + 1);
......@@ -227,6 +238,11 @@ function uploadcsv_validate(Pieform $form, $values) {
$field = preg_replace('/^(\s|\xc2\xa0)*(.*?)(\s|\xc2\xa0)*$/', '$2', $field);
}
if (count($line) != count($csvdata->format)) {
CSVErrors::add($i, get_string('uploadcsverrorwrongnumberoffields', 'admin', $i));
continue;
}
// We have a line with the correct number of fields, but should validate these fields
// Note: This validation should really be methods on each profile class, that way
// it can be used in the profile screen as well.
......@@ -246,22 +262,117 @@ function uploadcsv_validate(Pieform $form, $values) {
CSVErrors::add($i, get_string('uploadcsverrorinvalidusername', 'admin', $i));
}
}
if (record_exists_select('usr', 'LOWER(username) = ?', strtolower($username)) || isset($usernames[strtolower($username)])) {
CSVErrors::add($i, get_string('uploadcsverroruseralreadyexists', 'admin', $i, $username));
if (!$values['updateusers']) {
// Note: only checks for valid form are done here, none of the checks
// like whether the password is too easy. The user is going to have to
// change their password on first login anyway.
if (method_exists($authobj, 'is_password_valid') && !$authobj->is_password_valid($password)) {
CSVErrors::add($i, get_string('uploadcsverrorinvalidpassword', 'admin', $i));
}
}
if (record_exists('usr', 'email', $email) || record_exists('artefact_internal_profile_email', 'email', $email) || isset($emails[$email])) {
if (isset($emails[$email])) {
// Duplicate email within this file.
CSVErrors::add($i, get_string('uploadcsverroremailaddresstaken', 'admin', $i, $email));
}
else if (!$values['updateusers']) {
// The email address must be new
if (record_exists('usr', 'email', $email) || record_exists('artefact_internal_profile_email', 'email', $email, 'verified', 1)) {
CSVErrors::add($i, get_string('uploadcsverroremailaddresstaken', 'admin', $i, $email));
}
}
$emails[$email] = 1;
// Note: only checks for valid form are done here, none of the checks
// like whether the password is too easy. The user is going to have to
// change their password on first login anyway.
if (method_exists($authobj, 'is_password_valid') && !$authobj->is_password_valid($password)) {
CSVErrors::add($i, get_string('uploadcsverrorinvalidpassword', 'admin', $i));
if (isset($usernames[strtolower($username)])) {
// Duplicate username within this file.
CSVErrors::add($i, get_string('uploadcsverroruseralreadyexists', 'admin', $i, $username));
}
else {
if (!$values['updateusers'] && record_exists_select('usr', 'LOWER(username) = ?', strtolower($username))) {
CSVErrors::add($i, get_string('uploadcsverroruseralreadyexists', 'admin', $i, $username));
}
$usernames[strtolower($username)] = array(
'username' => $username,
'password' => $password,
'email' => $email,
'lineno' => $i,
'raw' => $line,
);
}
}
$usernames[strtolower($username)] = 1;
$emails[$email] = 1;
// If the admin is trying to overwrite existing users, identified by username,
// this second pass performs some additional checks
if ($values['updateusers']) {
foreach ($usernames as $lowerusername => $data) {
$line = $data['lineno'];
$username = $data['username'];
$password = $data['password'];
$email = $data['email'];
// If the user already exists, they must already be in this institution.
$userinstitutions = get_records_sql_assoc("
SELECT COALESCE(ui.institution, 'mahara') AS institution, u.id
FROM {usr} u LEFT JOIN {usr_institution} ui ON u.id = ui.usr
WHERE LOWER(u.username) = ?",
array($lowerusername)
);
if ($userinstitutions) {
if (!isset($userinstitutions[$institution])) {
if ($institution == 'mahara') {
$institutiondisplay = array();
foreach ($userinstitutions as $i) {
$institutiondisplay[] = $INSTITUTIONNAME[$i->institution];
}
$institutiondisplay = join(', ', $institutiondisplay);
$message = get_string('uploadcsverroruserinaninstitution', 'admin', $line, $username, $institutiondisplay);
}
else {
$message = get_string('uploadcsverrorusernotininstitution', 'admin', $line, $username, $INSTITUTIONNAME[$institution]);
}
CSVErrors::add($line, $message);
}
else {
// Remember that this user is being updated
$UPDATES[$username] = 1;
}
}
else {
// New user, check the password
if (method_exists($authobj, 'is_password_valid') && !$authobj->is_password_valid($password)) {
CSVErrors::add($line, get_string('uploadcsverrorinvalidpassword', 'admin', $line));
}
}
// It's okay if the email already exists and is owned by this user.
$emailowned = get_record_sql('
SELECT LOWER(u.username) AS lowerusername, ae.principal FROM {usr} u
LEFT JOIN {artefact_internal_profile_email} ae ON u.id = ae.owner AND ae.verified = 1 AND ae.email = ?
WHERE ae.owner IS NOT NULL OR u.email = ?',
array($email, $email)
);
// If the email is owned by someone else, it could still be okay provided
// that other user's email is also being changed in this csv file.
if ($emailowned && $emailowned->lowerusername != $lowerusername) {
if (!$emailowned->principal) {
// However, only primary emails can be set in uploadcsv, so this is an error
CSVErrors::add($line, get_string('uploadcsverroremailaddresstaken', 'admin', $line, $email));
}
else if (!isset($usernames[$emailowned->lowerusername])) {
// The other user is not being updated in this file
CSVErrors::add($line, get_string('uploadcsverroremailaddresstaken', 'admin', $line, $email));
}
else {
// If the other user is being updated in this file, but isn't changing their
// email address, it's ok, we've already notified duplicate emails within the file.
}
}
}
}
if ($errors = CSVErrors::process()) {
......@@ -278,12 +389,13 @@ function uploadcsv_validate(Pieform $form, $values) {
* password on next login also.
*/
function uploadcsv_submit(Pieform $form, $values) {
global $SESSION, $CSVDATA, $FORMAT;
global $SESSION, $CSVDATA, $FORMAT, $UPDATES;
$formatkeylookup = array_flip($FORMAT);
$authinstance = (int) $values['authinstance'];
$authobj = get_record('auth_instance', 'id', $authinstance);
$authrecord = get_record('auth_instance', 'id', $authinstance);
$authobj = AuthFactory::create($authinstance);
$institution = new Institution($authobj->institution);
......@@ -298,7 +410,12 @@ function uploadcsv_submit(Pieform $form, $values) {
}
}
log_info('Inserting users from the CSV file');
if ($values['updateusers']) {
log_info('Updating users from the CSV file');
}
else {
log_info('Inserting users from the CSV file');
}
db_begin();
$addedusers = array();
......@@ -310,7 +427,6 @@ function uploadcsv_submit(Pieform $form, $values) {
}
foreach ($CSVDATA as $record) {
log_debug('adding user ' . $record[$formatkeylookup['username']]);
$user = new StdClass;
$user->authinstance = $authinstance;
$user->username = $record[$formatkeylookup['username']];
......@@ -326,7 +442,6 @@ function uploadcsv_submit(Pieform $form, $values) {
if (isset($formatkeylookup['preferredname'])) {
$user->preferredname = $record[$formatkeylookup['preferredname']];
}
$user->passwordchange = (int)$values['forcepasswordchange'];
$profilefields = new StdClass;
$remoteuser = null;
......@@ -343,9 +458,27 @@ function uploadcsv_submit(Pieform $form, $values) {
$profilefields->{$field} = $record[$formatkeylookup[$field]];
}
$user->id = create_user($user, $profilefields, $institution, $authobj, $remoteuser);
if (!$values['updateusers'] || !isset($UPDATES[$user->username])) {
$user->passwordchange = (int)$values['forcepasswordchange'];
$addedusers[] = $user;
$user->id = create_user($user, $profilefields, $institution, $authrecord, $remoteuser);
reset_password($user, false);
$addedusers[] = $user;
log_debug('added user ' . $user->username);
}
else if (isset($UPDATES[$user->username])) {
$updated = update_user($user, $profilefields, $remoteuser);
if (empty($updated)) {
// Nothing changed for this user
unset($UPDATES[$user->username]);
}
else {
$UPDATES[$user->username] = $updated;
log_debug('updated user ' . $user->username . ' (' . implode(', ', array_keys($updated)) . ')');
}
}
}
db_commit();
......@@ -381,13 +514,18 @@ function uploadcsv_submit(Pieform $form, $values) {
}
}
foreach ($addedusers as $user) {
reset_password($user, false);
}
log_info('Inserted ' . count($CSVDATA) . ' records');
log_info('Added ' . count($addedusers) . ' users, updated ' . count($UPDATES) . ' users.');
$SESSION->add_ok_msg(get_string('uploadcsvusersaddedsuccessfully', 'admin'));
$SESSION->add_ok_msg(get_string('csvfileprocessedsuccessfully', 'admin'));
if ($UPDATES) {
$updatemsg = smarty_core();
$updatemsg->assign('added', count($addedusers));
$updatemsg->assign('updates', $UPDATES);
$SESSION->add_info_msg($updatemsg->fetch('admin/users/csvupdatemessage.tpl'), false);
}
else {
$SESSION->add_ok_msg(get_string('numbernewusersadded', 'admin', count($addedusers)));
}
redirect('/admin/users/uploadcsv.php');
}
......
......@@ -414,6 +414,7 @@ $string['uploadcsverrorinvalidfieldname'] = 'The field name "%s" is invalid, or
$string['uploadcsverrorrequiredfieldnotspecified'] = 'A required field "%s" has not been specified in the format line';
$string['uploadcsverrornorecords'] = 'The file appears to contain no records (although the header is fine)';
$string['uploadcsverrorunspecifiedproblem'] = 'The records in your CSV file could not be inserted for some reason. If your file is in the correct format then this is a bug and you should <a href="https://eduforge.org/tracker/?func=add&group_id=176&atid=739">create a bug report</a>, attaching the CSV file (remember to blank out passwords!) and, if possible, the error log file';
$string['uploadcsverrorwrongnumberoffields'] = 'Error on line %s of your file: Incorrect number of fields';
$string['uploadcsverrorinvalidemail'] = 'Error on line %s of your file: The e-mail address for this user is not in correct form';
$string['uploadcsverrorincorrectnumberoffields'] = 'Error on line %s of your file: This line does not have the correct number of fields';
$string['uploadcsverrorinvalidpassword'] = 'Error on line %s of your file: Passwords must be at least six characters long and contain at least one digit and two letters';
......@@ -421,6 +422,8 @@ $string['uploadcsverrorinvalidusername'] = 'Error on line %s of your file: The u
$string['uploadcsverrormandatoryfieldnotspecified'] = 'Line %s of the file does not have the required "%s" field';
$string['uploadcsverroruseralreadyexists'] = 'Line %s of the file specifies the username "%s" that already exists';
$string['uploadcsverroremailaddresstaken'] = 'Line %s of the file specifies the e-mail address "%s" that is already taken by another user';
$string['uploadcsverrorusernotininstitution'] = 'Error on line %s: The user "%s" is not a member of the institution %s.';
$string['uploadcsverroruserinaninstitution'] = 'Error on line %s: The user "%s" is a member of the following institutions: %s. You cannot update this user\'s authentication method to No Institution.';
$string['uploadcsvpagedescription2'] = '<p>You may use this facility to upload new users via a <acronym title="Comma Separated Values">CSV</acronym> file.</p>
<p>The first row of your CSV file should specify the format of your CSV data. For example, it should look like this:</p>
......@@ -444,8 +447,14 @@ $string['uploadcsvpagedescription2institutionaladmin'] = '<p>You may use this fa
%s';
$string['uploadcsvsomeuserscouldnotbeemailed'] = 'Some users could not be e-mailed. Their e-mail addresses may be invalid, or the server Mahara is running on might not be configured to send e-mail properly. The server error log has more details. For now, you may want to contact these people manually:';
$string['uploadcsvusersaddedsuccessfully'] = 'The users in the file have been added successfully';
$string['uploadcsvfailedusersexceedmaxallowed'] = 'No users have been added because there are too many users in your file. The number of users in the institution would have exceeded the maximum number allowed.';
$string['updateusers'] = 'Update Users';
$string['updateusersdescription'] = 'If your CSV file contains the usernames of users who are already members of the institution you have specified, their details will be overwritten with data from the file. Use with care.';
$string['csvfileprocessedsuccessfully'] = 'Your CSV file was processed successfully';
$string['nousersadded'] = 'No users were added.';
$string['numbernewusersadded'] = 'New users added: %s.';
$string['numberusersupdated'] = 'Users updated: %d.';
$string['showupdatedetails'] = 'Show update details';
// Bulk Leap2A import
$string['bulkleap2aimport'] = 'Import users from Leap2A files';
......
......@@ -1778,6 +1778,73 @@ function create_user($user, $profile=array(), $institution=null, $remoteauth=nul
return $user->id;
}
/**
* Update user
*
* @param object $user stdclass for the usr table
* @param object $profile profile field/values to set
* @param string $remotename username on the remote site
* @return array list of updated fields
*/
function update_user($user, $profile, $remotename=null) {
require_once(get_config('docroot') . 'auth/session.php');
if (!empty($user->id)) {
$oldrecord = get_record('usr', 'id', $user->id);
}
else {
$oldrecord = get_record('usr', 'username', $user->username);
}
$userid = $oldrecord->id;
db_begin();
// Log the user out, otherwise they can overwrite all this on the next request
remove_user_sessions($userid);
$updated = array();
$newrecord = new StdClass;
foreach (get_object_vars($user) as $k => $v) {
if (!empty($v) && ($k == 'password' || empty($oldrecord->$k) || $oldrecord->$k != $v)) {
$newrecord->$k = $v;
$updated[$k] = $v;
}
}
if (count(get_object_vars($newrecord))) {
$newrecord->id = $userid;
update_record('usr', $newrecord);
if (!empty($newrecord->password)) {
$newrecord->authinstance = $user->authinstance;
reset_password($newrecord, false);
}
}
foreach (get_object_vars($profile) as $k => $v) {
if (get_profile_field($userid, $k) != $v) {
set_profile_field($userid, $k, $v);
$updated[$k] = $v;
}
}
if ($remotename) {
$oldremote = get_field('auth_remote_user', 'remoteusername', 'authinstance', $oldrecord->authinstance, 'localusr', $userid);
if ($remotename != $oldremote) {
$updated['remoteuser'] = $remotename;
}
delete_records('auth_remote_user', 'authinstance', $user->authinstance, 'localusr', $userid);
delete_records('auth_remote_user', 'authinstance', $user->authinstance, 'remoteusername', $remotename);
insert_record('auth_remote_user', (object) array(
'authinstance' => $user->authinstance,
'remoteusername' => $remotename,
'localusr' => $userid,
));
}
db_commit();
return $updated;
}
/**
* Given a user, makes sure they have been added to all groups that are marked
......
{if $added}{str tag=numbernewusersadded section=admin arg1=$added}{else}{str tag=nousersadded section=admin}{/if}
{str tag=numberusersupdated section=admin arg1=count($updates)}
<a href="" onclick="toggleElementClass('hidden', 'csvupdateinfo'); return false;">{str tag=showupdatedetails section=admin}</a>
<div id="csvupdateinfo" class="hidden">
{foreach from=$updates key=username item=fields}{strip}
<div>&nbsp;{$username}:&nbsp;
{foreach from=$fields key=k item=v name=fields}
{$k} &rarr; {$v}{if !$dwoo.foreach.fields.last},&nbsp;{/if}
{/foreach}
</div>{/strip}
{/foreach}
</div>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment