= *
* $min_user_editing_level) can edit other users, and they *
* cannot edit users with a higher level than themselves *
* *
* *
\*****************************************************************************/
require "defaultincludes.inc";
if (!auth()->canCreateUsers())
{
// You shouldn't be here unless you can create users
location_header('index.php');
}
// Get non-standard form variables
$action = get_form_var('action', 'string');
$id = get_form_var('id', 'int');
$password0 = get_form_var('password0', 'string', null, INPUT_POST);
$password1 = get_form_var('password1', 'string', null, INPUT_POST);
$invalid_email = get_form_var('invalid_email', 'int');
$name_empty = get_form_var('name_empty', 'int');
$name_not_unique = get_form_var('name_not_unique', 'int');
$taken_name = get_form_var('taken_name', 'string');
$pwd_not_match = get_form_var('pwd_not_match', 'string');
$pwd_invalid = get_form_var('pwd_invalid', 'string');
$invalid_dates = get_form_var('invalid_dates', 'array');
$datatable = get_form_var('datatable', 'int'); // Will only be set if we're using DataTables
$back_button = get_form_var('back_button', 'string');
$delete_button = get_form_var('delete_button', 'string');
$edit_button = get_form_var('edit_button', 'string');
$update_button = get_form_var('update_button', 'string');
if (isset($back_button))
{
unset($action);
}
elseif (isset($delete_button))
{
$action = 'delete';
}
elseif (isset($edit_button))
{
$action = 'edit';
}
elseif (isset($update_button))
{
$action = 'update';
}
// 等保优化(2026-09-08):管理员新建用户时若未填写密码,自动使用默认初始密码
// “Lzolj+当前年份”(如 Lzolj2026),免去每次手填;该用户首登将被强制改密。
// 仅在“新建用户提交”场景生效(action=update 且 $id 未设置);编辑既有用户留空不改密不受影响。
// 注意:本段必须放在上方 $action 被重设为 'update' 之后,否则 $action 仍为 'add' 导致判断失效。
// 密码框留空时表单仍提交空字符串(键存在值为空),故用 === '' 即可命中;同时为防字段缺失返回 null 一并判断。
// $used_default_password 标记本次是否使用了默认密码,供下方复杂度校验放行(默认密码本就不含特殊字符)。
$used_default_password = false;
if (($action == 'update') && !isset($id) && ($password0 === '' || $password0 === null))
{
$default_password = 'Lzolj' . date('Y');
$password0 = $default_password;
$password1 = $default_password;
$used_default_password = true;
}
$is_ajax = is_ajax();
// Checks whether the current user can view the target user
function can_view_user($target)
{
global $auth, $min_user_viewing_level;
$mrbs_user = session()->getCurrentUser();
// You can only see this user if you are logged in and (a) we allow everybody to see all
// users or (b) you are an admin or (c) you are this user
if (!isset($mrbs_user))
{
return false;
}
return (!$auth['only_admin_can_see_other_users'] ||
($mrbs_user->level >= $min_user_viewing_level) ||
(compare_usernames($mrbs_user->username, $target) === 0));
}
// Checks whether the current user can edit the target user
function can_edit_user($target)
{
$mrbs_user = session()->getCurrentUser();
return (is_user_admin() || (isset($mrbs_user) && compare_usernames($mrbs_user->username, $target) === 0));
}
function output_row($row)
{
global $is_ajax, $json_data;
global $fields, $ignore_columns, $select_options;
$values = array();
// First column, which is the display name
// Make sure we've got a display name. If not, use the username.
if (!isset($row['display_name']) || (trim($row['display_name']) === ''))
{
$row['display_name'] = $row['name'];
}
// You can only edit a user if you have sufficient admin rights, or else if that user is yourself
if (can_edit_user($row['name']))
{
$form = new Form(Form::METHOD_POST);
$form->setAttributes(array('action' => multisite(this_page())));
$form->addHiddenInput('id', $row['id']);
$submit = new ElementInputSubmit();
$submit->setAttributes(array('class' => 'link',
'name' => 'edit_button',
'value' => $row['display_name']));
$form->addElement($submit);
$display_name_value = $form->toHTML();
}
else
{
$display_name_value = "" . escape_html($row['display_name']) . "";
}
$sortname = get_sortable_name($row['display_name']);
// TODO: move the data-order attribute up into the
and get rid of the
$values[] = '' . $display_name_value;
// Then the username
$values[] = '' . escape_html($row['name']) . '';
// Other columns
foreach ($fields as $field)
{
$key = $field['name'];
if (!in_array($key, $ignore_columns))
{
$col_value = $row[$key];
// If you are not a user admin then you are only allowed to see the last_updated
// and last_login times for yourself.
if (in_array($key, array('timestamp', 'last_login')) &&
!can_edit_user($row['name']))
{
$col_value = null;
}
switch($key)
{
// special treatment for some fields
case 'level':
// the level field contains a code and we want to display a string
// (but we put the code in a span for sorting)
$values[] = '' .
"
" . get_vocab("level_$col_value") . "
";
break;
case 'email':
// we don't want to truncate the email address
if (isset($col_value) && ($col_value !== ''))
{
$escaped_email = escape_html($col_value);
$values[] = "
\n";
}
else
{
$values[] = '';
}
break;
case 'timestamp':
// Use the 'last_updated' value because it will have been converted
// from 'timestamp' using the correct timezone, ie the timezone of
// the database server.
$col_value = $row['last_updated'];
// Fall through
case 'last_login':
// Put the UNIX timestamp in a span so that the JavaScript can sort it properly.
// If there isn't a date then put '0' in the title, otherwise DataTables throws
// a TypeError if you try and sort the column.
$values[] = '' .
(($col_value) ? escape_html(time_date_string($col_value)) : '');
break;
default:
// Where there's an associative array of options, display
// the value rather than the key
if (isset($select_options["users.$key"]) &&
is_assoc($select_options["users.$key"]))
{
if (isset($row[$key]) && isset($select_options["users.$key"][$row[$key]]))
{
$col_value = $select_options["users.$key"][$row[$key]];
}
else
{
$col_value = '';
}
$values[] = "
";
}
break;
} // end switch
}
} // end foreach
if ($is_ajax)
{
$json_data['aaData'][] = $values;
}
else
{
echo "
\n
\n";
echo implode("
\n
", $values);
echo "
\n
\n";
}
}
function get_field_level($params, $disabled=false)
{
global $level;
// Only display options up to and including one's own level (you can't upgrade yourself).
// If you're not some kind of admin then the select will also be disabled.
// (Note - disabling individual options doesn't work in older browsers, eg IE6)
$options = array();
for ($i=0; $i<=$level; $i++)
{
$options[$i] = get_vocab("level_$i");
}
$field = new FieldSelect();
$field->setLabel($params['label'])
->setControlAttributes(array('name' => $params['name'],
'disabled' => $disabled))
->addSelectOptions($options, $params['value'], true);
return $field;
}
function get_field_name($params, $disabled=false)
{
$field = new FieldInputText();
$field->setLabel($params['label'])
->setControlAttributes(array('name' => $params['name'],
'value' => $params['value'],
'disabled' => $disabled,
'required' => true,
'pattern' => REGEX_TEXT_POS));
if (null !== ($maxlength = maxlength('users.name')))
{
$field->setControlAttribute('maxlength', $maxlength);
}
// If the name field is disabled we need to add a hidden input, because
// otherwise it won't be posted.
if ($disabled)
{
$field->addHiddenInput($params['name'], $params['value']);
}
return $field;
}
function get_field_display_name($params, $disabled=false)
{
$field = new FieldInputText();
$field->setLabel($params['label'])
->setControlAttributes(array('name' => $params['name'],
'value' => $params['value'],
'disabled' => $disabled,
'required' => true,
'pattern' => REGEX_TEXT_POS));
if (null !== ($maxlength = maxlength('users.display_name')))
{
$field->setControlAttribute('maxlength', $maxlength);
}
// If the name field is disabled we need to add a hidden input, because
// otherwise it won't be posted.
if ($disabled)
{
$field->addHiddenInput($params['name'], $params['value']);
}
return $field;
}
function get_field_email($params, $disabled=false)
{
$field = new FieldInputEmail();
$field->setLabel($params['label'])
->setControlAttributes(array('name' => $params['name'],
'value' => $params['value'],
'disabled' => $disabled));
if (null !== ($maxlength = maxlength('users.email')))
{
$field->setControlAttribute('maxlength', $maxlength);
}
return $field;
}
function get_field_custom($custom_field, $params, $disabled=false)
{
global $select_options, $datalist_options, $is_mandatory_field, $pattern;
global $text_input_max;
// TODO: have a common way of generating custom fields for all tables
// Output a checkbox if it's a boolean or integer <= 2 bytes (which we will
// assume are intended to be booleans)
if (($custom_field['nature'] == 'boolean') ||
(($custom_field['nature'] == 'integer') && isset($custom_field['length']) && ($custom_field['length'] <= 2)) )
{
$class = 'FieldInputCheckbox';
}
// Otherwise check if it's an integer field
elseif ((($custom_field['nature'] == 'integer') && ($custom_field['length'] > 2)) ||
($custom_field['nature'] == 'decimal'))
{
$class = 'FieldInputNumber';
}
elseif (!empty($select_options[$params['field']]))
{
$class = 'FieldSelect';
}
elseif (!empty($datalist_options[$params['field']]))
{
$class = 'FieldInputDatalist';
}
// Output a textarea if it's a character string longer than the limit for a
// text input
elseif (($custom_field['nature'] == 'character') && isset($custom_field['length']) && ($custom_field['length'] > $text_input_max))
{
$class = 'FieldTextarea';
}
elseif ($custom_field['type'] == 'date')
{
$class = 'FieldInputDate';
}
else
{
$class = 'FieldInputText';
}
$full_class = __NAMESPACE__ . "\\Form\\$class";
$field = new $full_class();
$field->setLabel($params['label'])
->setControlAttribute('name', $params['name']);
if ($custom_field['nature'] == 'decimal')
{
list( , $decimal_places) = explode(',', $custom_field['length']);
$decimal_places = intval($decimal_places);
$step = pow(10, -$decimal_places);
$step = number_format($step, $decimal_places);
$field->setControlAttribute('step', $step);
}
if (!empty($is_mandatory_field[$params['field']]))
{
$field->setControlAttribute('required', true);
}
if ($disabled)
{
$field->setControlAttribute('disabled', true);
$field->addHiddenInput($params['name'], $params['value']);
}
// Pattern attribute, if any
if (!empty($pattern[$params['field']]))
{
$field->setControlAttribute('pattern', $pattern[$params['field']]);
// And any custom error messages
$tag = $params['field'] . '.oninvalid';
$oninvalid_text = get_vocab($tag);
if (isset($oninvalid_text) && ($oninvalid_text !== $tag))
{
$field->setControlAttribute('oninvalid', "this.setCustomValidity('". escape_js($oninvalid_text) . "')");
// Need to clear the invalid message
$field->setControlAttribute('onchange', "this.setCustomValidity('')");
}
}
switch ($class)
{
case 'FieldInputCheckbox':
$field->setChecked($params['value']);
break;
case 'FieldSelect':
$options = $select_options[$params['field']];
$field->addSelectOptions($options, $params['value']);
break;
case 'FieldInputDate':
case 'FieldInputNumber':
$field->setControlAttribute('value', $params['value']);
break;
case 'FieldInputDatalist':
$options = $datalist_options[$params['field']];
$field->addDatalistOptions($options);
// Drop through
case 'FieldInputText':
if (!empty($is_mandatory_field[$params['field']]))
{
// Set a pattern as well as required to prevent a string of whitespace
$field->setControlAttribute('pattern', REGEX_TEXT_POS);
}
// Drop through
case 'FieldTextarea':
if ($class == 'FieldTextarea')
{
$field->setControlText($params['value'] ?? '');
}
else
{
$field->setControlAttribute('value', $params['value']);
}
if (null !== ($maxlength = maxlength($params['field'])))
{
$field->setControlAttribute('maxlength', $maxlength);
}
break;
default:
throw new \Exception("Unknown class '$class'");
break;
}
return $field;
}
function get_fieldset_password($id=null, $disabled=false)
{
$fieldset = new ElementFieldset();
// If this is an existing user then give them the message about optionally
// changing their password.
if (isset($id))
{
$p = new ElementP();
$p->setText(get_vocab('password_twice'));
$fieldset->addElement($p);
}
for ($i=0; $i<2; $i++)
{
$field = new FieldInputPassword();
$field->setLabel(get_vocab('users.password'))
->setControlAttributes(array('id' => "password$i",
'name' => "password$i",
'disabled' => $disabled,
'autocomplete' => 'new-password'));
// No need to add a hidden input if the password is disabled because
// we don't put the password in the form anyway.
$fieldset->addElement($field);
}
return $fieldset;
}
// Adds the submit buttons.
// $delete If true, make the second button a Delete button instead of a Back button
// $disabled If true, disable the Delete button
// $last_admin_warning If true, add a warning about editing the last admin
function get_fieldset_submit_buttons(?int $user_id, $delete=false, $disabled=false, $last_admin_warning=false)
{
global $auth;
// Check whether the user can be deleted and if so what message to use
if ($delete && isset($user_id))
{
$user = auth()->getUserByUserId($user_id);
// If the user has bookings of some form in the system, then either
// disallow the deletion, depending on the config setting, or provide
// a different warning message.
if (isset($user) && $user->isInBookings())
{
if ($auth['db']['prevent_deletion_of_users_in_bookings'])
{
$delete = false;
}
else
{
$message = get_vocab("confirm_delete_user_plus");
}
}
else
{
$message = get_vocab("confirm_delete_user");
}
}
$fieldset = new ElementFieldset();
if ($last_admin_warning)
{
$p = new ElementP();
$p->setText(get_vocab('warning_last_admin'));
$fieldset->addElement($p);
}
$field = new FieldInputSubmit();
$button = new ElementInputSubmit();
if ($delete)
{
$name = 'delete_button';
$value = get_vocab('delete_user');
}
else
{
$name = 'back_button';
$value = get_vocab('back');
}
$button->setAttributes(array('name' => $name,
'value' => $value,
'disabled' => $disabled,
'formnovalidate' => true));
if ($delete)
{
$button->setAttribute('onclick', "return confirm('" . escape_js($message) . "');");
}
$field->setAttribute('class', 'submit_buttons')
->setLabelAttribute('class', 'no_suffix')
->addLabelElement($button)
->setControlAttributes(array('class' => 'default_action',
'name' => 'update_button',
'value' => get_vocab('save')));
// Remove the 'for' attribute which will automatically have been set by
// setControlAttributes(). It is unnecessary and leaving it in results in
// an HTML5 validation error.
$field->removeLabelAttribute('for');
$fieldset->addElement($field);
return $fieldset;
}
// Set up for Ajax. We need to know whether we're capable of dealing with Ajax
// requests, which will only be if the browser is using DataTables. We also need
// to initialise the JSON data array.
$ajax_capable = $datatable;
if ($is_ajax)
{
$json_data['aaData'] = array();
}
// Get the information about the fields in the users table
$fields = db()->field_info(_tbl('users'));
$users = auth()->getUsers();
/*---------------------------------------------------------------------------*\
| Authenticate the current user |
\*---------------------------------------------------------------------------*/
// Check the CSRF token if we're going to be altering the database
if (isset($action) && in_array($action, array('delete', 'update')))
{
Form::checkToken();
}
$initial_user_creation = false;
if (count($users) > 0)
{
$mrbs_user = session()->getCurrentUser();
$level = (isset($mrbs_user)) ? $mrbs_user->level : 0;
// Check the user is authorised for this page
checkAuthorised(this_page());
}
else
// We've just created the table. Assume the person doing this IS an administrator
// and then send them through to the screen to add the first user (which we'll force
// to be an admin)
{
$initial_user_creation = true;
if (!isset($action)) // second time through it will be set to "update"
{
$action = "add";
$id = null;
}
$level = $max_level;
}
/*---------------------------------------------------------------------------*\
| Edit a given entry - 1st phase: Get the user input. |
\*---------------------------------------------------------------------------*/
if (isset($action) && ( ($action == "edit") or ($action == "add") ))
{
if (isset($id))
{
// If it's an existing user then get the data from the database
$sql = "SELECT *
FROM " . _tbl('users') . "
WHERE id=?";
$result = db()->query($sql, array($id));
$data = $result->next_row_keyed();
unset($result);
// Check that we've got a valid result. We should do normally, but if somebody alters
// the id parameter in the query string then we won't. If the result is invalid, go somewhere
// safe.
if (!$data)
{
trigger_error("Invalid user id $id", E_USER_NOTICE);
location_header(this_page());
}
}
if (!isset($id) || (!$data))
{
// Otherwise try and get the data from the query string, and if it's
// not there set the default to be blank. (The data will be in the
// query string if there was an error on validating the data after it
// had been submitted. We want to preserve the user's original values
// so that they don't have to re-type them).
foreach ($fields as $field)
{
$type = get_form_var_type($field);
$value = get_form_var($field['name'], $type);
$data[$field['name']] = (isset($value)) ? $value : "";
}
}
// First make sure the user is authorized
if (!$initial_user_creation && !can_edit_user($data['name']))
{
showAccessDenied();
exit();
}
$context = array(
'view' => $view,
'view_all' => $view_all,
'year' => $year,
'month' => $month,
'day' => $day,
'area' => isset($area) ? $area : null,
'room' => isset($room) ? $room : null
);
print_header($context);
echo "
\n";
}
// Find out how many admins are left in the table - it's disastrous if the last one is deleted,
// or admin rights are removed!
if ($action == "edit")
{
$sql = "SELECT COUNT(*)
FROM " . _tbl('users') . "
WHERE level=?";
$n_admins = db()->query1($sql, array($max_level));
$editing_last_admin = ($n_admins <= 1) && ($data['level'] == $max_level);
}
else
{
$editing_last_admin = false;
}
// Error messages
if (!empty($invalid_email))
{
echo "
\n";
}
}
// Now do any password error messages
if (!empty($pwd_not_match))
{
echo "
" . get_vocab("passwords_not_eq") . "
\n";
}
if (!empty($pwd_invalid))
{
echo "
" . get_vocab("password_invalid") . "
\n";
if (isset($pwd_policy))
{
echo "
\n";
foreach ($pwd_policy as $rule => $value)
{
if ($value != 0)
{
echo "
" . get_vocab('policy_' . $rule, $value) . "
\n";
}
}
echo "
\n";
}
}
$form = new Form(Form::METHOD_POST);
$form->setAttributes(array('id' => 'form_edit_users',
'class' => 'standard',
'action' => multisite(this_page())));
if (isset($id))
{
$form->addHiddenInput('id', $id);
}
$fieldset = new ElementFieldset();
foreach ($fields as $field)
{
$key = $field['name'];
$params = array('label' => get_loc_field_name(_tbl('users'), $key),
'name' => VAR_PREFIX . $key,
'value' => $data[$key]);
$disabled = !$initial_user_creation &&
!is_user_admin() &&
in_array($key, $auth['db']['protected_fields']);
switch ($key)
{
case 'id': // We've already got this in a hidden input
case 'password_hash': // We don't want to do anything with this
case 'timestamp': // Nor this
case 'last_login':
case 'reset_key_hash':
case 'reset_key_expiry':
// 等保整改:以下安全控制列由系统自动维护,不允许在表单中直接编辑
case 'password_changed_at':
case 'failed_logins':
case 'locked_until':
break;
case 'level':
if ($action == 'add')
{
// If we're creating a new user and it's the very first user, then they
// should have maximum rights. Otherwise make them an ordinary user.
$params['value'] = ($initial_user_creation) ? $max_level : 1;
}
// Work out whether the level select input should be disabled (NB you can't make a