= * * $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'; } $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" . "$escaped_email\n" . "
\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[] = "
" . escape_html($col_value) . "
"; } elseif (($field['nature'] == 'boolean') || (($field['nature'] == 'integer') && isset($field['length']) && ($field['length'] <= 2)) ) { // booleans: represent by a checkmark $values[] = (!empty($col_value)) ? "✓" : ''; } elseif (($field['nature'] == 'integer') && isset($field['length']) && ($field['length'] > 2)) { // integer values $values[] = $col_value; } else { // strings if (!isset($col_value)) { $col_value = ''; } $values[] = "
" . escape_html($col_value) . "
"; } 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 "

"; if ($initial_user_creation) { echo get_vocab('no_users_initial'); } else { echo ($action == 'edit') ? get_vocab('edit_user') : get_vocab('add_new_user'); } echo "

\n"; if ($initial_user_creation) { echo "

" . get_vocab('no_users_create_first_admin') . "

\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 "

" . get_vocab('invalid_email') . "

\n"; } if (!empty($name_not_unique)) { echo "

'" . escape_html($taken_name) . "' " . get_vocab('name_not_unique') . "

\n"; } if (!empty($name_empty)) { echo "

" . get_vocab('name_empty') . "

\n"; } if (!empty($invalid_dates)) { foreach ($invalid_dates as $field) { echo "

" . get_vocab('invalid_date', get_loc_field_name(_tbl('users'), $field)) . "

\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"; } } $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