Files
人事系统开发 1ba6efd8ed MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、JS空集合保护、
会话过期体验优化(403 JSON)、display_errors 关闭、
固定 key 根治 Integrity check failed 等全部改动

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
2026-09-09 16:55:02 +08:00

486 lines
18 KiB
PHP

<?php
declare(strict_types=1);
namespace MRBS;
require "../defaultincludes.inc";
http_headers(array("Content-type: application/x-javascript"),
60*30); // 30 minute expiry
?>
'use strict';
<?php
global $autocomplete_length_breaks;
// Function to determine whether the browser supports the HTML5
// <datalist> element.
?>
var supportsDatalist = function supportsDatalist() {
<?php
// The first two conditions work for most browsers. The third condition is
// necessary for Safari, which, certainly for versions up to 6.0, the latest at
// the time of writing, return true for the first two conditions even though
// it doesn't support <datalist>.
?>
return ('list' in document.createElement('input')) &&
('options' in document.createElement('datalist')) &&
(window.HTMLDataListElement !== undefined);
};
<?php
// If we are operating on a wide screen when the standard form fieldsets are
// displayed as tables, then make sure that the left hand column in the standard
// form is of constant width. If there are multiple fieldsets then each fieldset
// will have its own width, as the display:table only applies to that fieldset.
?>
var adjustLabelWidths = function adjustLabelWidths() {
var standardFieldset = $('.standard fieldset');
if ((standardFieldset.length !== 0) && (standardFieldset.css('display') === 'table'))
{
var labels = standardFieldset.children('div').children('label').not('.rep_type_details label');
<?php // Let the labels find their natural widths ?>
labels.width('auto');
<?php // Add on one pixel to avoid what look to be like rounding problems in some browsers ?>
labels.width(getMaxWidth(labels) + 1);
}
}
function fillUsernameFields()
{
var select = $('.ajax_usernames');
<?php // We don't want to fire off an unnecessary POST request ?>
if (select.length === 0)
{
return;
}
select.each(function() {
<?php // Turn the create_by select into a fancy select box. ?>
var el = $(this);
el.mrbsSelect(el.hasClass('datalist'));
<?php
// Add a class to the body so that we can modify the CSS when the load
// is in progress, eg by adding an animated GIF. We remove the class
// once the Ajax data has arrived.
?>
$('body').addClass('ajax-loading');
});
<?php
// Fire off an Ajax request to get the data. We do this because some authentication
// schemes, eg LDAP, will take a long time to return the data if there are very many
// users and we don't want to hold up the page load. Most of the time the data won't
// even be needed anyway because the booking will be made in the name of the current
// user.
//
// Select2 offers an Ajax option, but it is not particularly suitable because (a) the
// Ajax request is not fired until the Select2 element is opened, which means the clock
// doesn't start ticking until then and (b) a new request is fired whenever the search
// term is changed. It does though offer some nice features such as pagination and
// query terms, but these still aren't going to help much. And LDAP searches of the
// form "*TERM*" can be expensive.
// See https://select2.org/data-sources/ajax for more details
?>
$.post({
url: 'ajax/usernames.php',
dataType: 'json',
data: {csrf_token: getCSRFToken(), site: args.site},
success: function(data) {
select.each(function() {
var el = $(this);
var newOption;
<?php
// Get the current option (there will only be one) so we know
// which one should be selected in the new list
// Convert usernames to strings before being converted to upper case
// in case the usernames look like ints, for example if ids are being
// used for usernames.
?>
var currentOption = el.find('option').first();
var currentValue = currentOption.val();
var currentValueUpper = currentValue.toString().toUpperCase();
var currentText = currentOption.text();
<?php
// Remove the existing option, because it will be in the new dataset in
// the correct position.
?>
el.empty();
<?php
// Add the new data, selecting the option that was previously selected
?>
var foundCurrent = false;
$.each(data, function(index, option) {
if (option.username !== null)
{
// Make it a case-insensitive comparison as usernames are case-insensitive
var selected = (option.username.toString().toUpperCase() === currentValueUpper);
foundCurrent = foundCurrent || selected;
var newOption = new Option(option.display_name, option.username, selected, selected);
el.append(newOption);
}
});
<?php
// It's possible that the creator of the booking is no longer a user (they may have left
// the organisation and been deleted from the user list). If that's the case and we haven't
// found them while running through the user list, then add them and make them the selected
// option. (Ideally the list should perhaps be sorted again, but then we'd have to worry
// about locales. And having the original creator at the end of the list perhaps draws attention
// to the fact that they no longer exist).
?>
if (!foundCurrent)
{
newOption = new Option(currentText, currentValue, true, true);
el.append(newOption);
}
<?php
// If there was one, close the Select2 control and refresh it. If it was open before the
// close, then reopen it after the refresh.
//
?>
if (el.hasClass('select2-hidden-accessible'))
{
var wasOpen = el.select2('isOpen');
el.select2('close').trigger('change');
if (wasOpen)
{
el.select2('open');
}
}
});
$('body').removeClass('ajax-loading');
}
});
}
var args;
$(document).on('page_ready', function() {
<?php // Retrieve the data that the JavaScript files need. ?>
args = $('body').data();
<?php // Fire off the Ajax requests for username fields ?>
fillUsernameFields();
<?php
// If we're required to log the user out after a period of inactivity then the user filling in
// an MRBS form counts as activity and we need to record it. In fact we'll record any key or
// mouse activity for this document as activity.
if (($auth["session"] == "php") && !empty($auth["session_php"]["inactivity_expire_time"]))
{
?>
var recordActivity = function recordActivity() {
var d = new Date(),
t = d.getTime()/1000;
<?php
// Only tell the server that there's been some user activity if we're coming up to
// the inactivity timeout
?>
if ((typeof recordActivity.lastRecorded === 'undefined') ||
((t - recordActivity.lastRecorded) > (<?php echo $auth["session_php"]["inactivity_expire_time"]?> - 1)))
{
recordActivity.lastRecorded = t;
var params = {activity: 1};
if(args.site)
{
params.site = args.site;
}
$.post('ajax/record_activity.php', params, function() {
});
}
};
$(document).on('keydown mousemove mousedown', function() {
recordActivity();
});
<?php
}
// Add in a hidden input to the header search forms so that we can tell if we are using DataTables
// (which will be if JavaScript is enabled). We need to know this because when we're using an
// Ajax data source we don't want to send the HTML version of the table data.
//
// Also add 'datatable=1' to the link for the user list for the same reason
?>
$('<input>').attr({
type: 'hidden',
name: 'datatable',
value: '1'
}).appendTo('form[action="search.php"]');
$('header a[href^="edit_users.php"]').each(function() {
var href = $(this).attr('href');
href += (href.indexOf('?') < 0) ? '?' : '&';
href += 'datatable=1';
$(this).attr('href', href);
});
<?php
// There are some forms that have multiple submit buttons, eg a "Back" and "Save"
// buttons. In these cases we want hitting the Enter key in a text input field
// to result in a "Save" rather than "Back". So in these cases we have assigned
// a class of 'default_action' to the one that we want to be executed when we hit
// Enter. (Note that it is a class rather than an id just in case we have two or
// more such forms on a page. However we should ensure that there is only one
// button with this class per form.)
?>
$('form input.default_action').each(function() {
var defaultSubmitButton = $(this);
$(this).parents('form').find('input').on('keypress', function(event) {
if (event.which === 13) // the Enter key
{
defaultSubmitButton.trigger('click');
return false;
}
else
{
return true;
}
});
});
<?php
// Where we've got enabling checkboxes, apply a change event to them so that
// when the enabling checkbox is changed the associated inputs are enabled or
// disabled as appropriate. Also trigger the change event when the page is loaded
// so that the inputs are enabled/disabled correctly initially.
?>
$('.enabler').on('change', function(){
var enablerChecked = $(this).is(':checked');
var elements;
switch ($(this).attr('name'))
{
<?php // Some of the groups are structured differently ?>
case 'area_max_duration_enabled':
elements = $('[name^="area_max_duration"]').not($(this));
break;
case 'registrant_limit_enabled':
elements = $('[name="registrant_limit"]');
break;
default:
elements = $(this).nextAll('input, select')
break;
}
elements.prop('disabled', !enablerChecked);
})
.trigger('change');
if (supportsDatalist())
{
<?php
// One problem with using a datalist with an input element is the way different browsers
// handle autocomplete. If you have autocomplete on, and also an id or name attribute, then some
// browsers, eg Edge, will bring the history up on top of the datalist options so that you can't
// see the first few options. But if you have autocomplete off, then other browsers, eg Chrome,
// will not present the datalist options at all. We fix this in JavaScript by having a second,
// hidden, input which holds the actual form value and mirrors the visible input. Because we can't
// rely on JavaScript being enabled we will create the basic HTML using autocomplete on, ie the default,
// which is the least bad alternative. One disadvantage of this method is that the label is no longer
// tied to the visible input, but this isn't as important for a text input as it is, say, for a checkbox
// or radio button.
?>
$('input[list]').each(function() {
var input = $(this),
hiddenInput = $('<input type="hidden">');
<?php
// Create a hidden input with the id, name and value of the original input. Then remove the id and
// name from the original input (so that history doesn't work). Finally make sure that
// the hidden input is updated whenever the original input is changed.
?>
hiddenInput.attr('id', input.attr('id'))
.attr('name', input.attr('name'))
.val(input.val());
input.removeAttr('id')
.removeAttr('name')
.after(hiddenInput);
<?php
// We use the 'input' rather than 'change' event because 'input' isn't fired in
// Edge when a datalist option is selected.
?>
input.on('input', function() {
hiddenInput.val($(this).val());
});
});
<?php
// Because there are some browsers, eg MSIE and Edge, that will still give you form history even
// though the input has no id or name, then we need to clear the values from those inputs just
// before the form is submitted. Note that we can't do it on the submit event because by that time
// the browser has cached the values. So we do it when the Submit button is clicked - and this event
// is also triggered if Enter is entered into an input field. But
// of course we can't clear the value if the input field needs to be
// validated, otherwise the validation will fail.
?>
$('form:has(input[list]) input[type="submit"]').on('click', function() {
$(this).closest('form')
.find('input:not([name])')
.not('input[type="submit"]')
.each(function() {
if (!$(this).prop('required') &&
(typeof($(this).attr('pattern')) == 'undefined'))
{
$(this).val('');
}
});
});
}
else
{
<?php
// Add jQuery UI Autocomplete functionality for those browsers that do not
// support the <datalist> element.
?>
$('datalist').each(function() {
var datalist = $(this);
var options = [];
datalist.parent().find('option').each(function() {
var option = {};
option.label = $(this).text();
option.value = $(this).val();
options.push(option);
});
var minLength = 0;
<?php
// Work out a suitable value for the autocomplete minLength
// option, ie the number of characters that must be typed before
// a list of options appears. We want to avoid presenting a huge
// list of options.
if (isset($autocomplete_length_breaks) && is_array($autocomplete_length_breaks))
{
?>
var breaks = [<?php echo implode(',', $autocomplete_length_breaks) ?>];
var nOptions = options.length;
var i=0;
while ((i<breaks.length) && (nOptions >= breaks[i]))
{
i++;
minLength++;
}
<?php
}
?>
var formInput = datalist.prev();
formInput.empty().autocomplete({
source: options,
minLength: minLength
});
<?php
// If the minLength is 0, then the autocomplete widget doesn't do
// quite what you might expect and you need to force it to display
// the available options when it receives focus
?>
if (minLength === 0)
{
formInput.on('focus', function() {
$(this).autocomplete('search', '');
});
}
});
}
<?php // Add a fallback for browsers that don't support the time input ?>
$('[type="time"]').each(function() {
if ($(this).prop('type') !== 'time')
{
$(this).attr('placeholder', 'hh:mm')
.attr('pattern', '<?php echo trim(REGEX_HHMM, '/') ?>')
.on('input', function(e) {
e.target.setCustomValidity('');
if (!e.target.validity.valid)
{
e.target.setCustomValidity('<?php echo get_js_vocab('invalid_time_format')?>');
}
});
}
});
adjustLabelWidths();
$(window)
<?php // Make resizing smoother by not redoing labels on every resize event ?>
.on('resize', throttle(function() {
adjustLabelWidths();
}, 100));
<?php // Add the eye icons for toggling password visibility ?>
$('input[type="password"]').each(function() {
$(this).after($('<div class="eye"><?php
echo(preg_replace('/[\r\n]/', ' ', file_get_contents(MRBS_ROOT . '/images/eye.svg')))
?></div>'))
.after($('<div class="eye off"><?php
echo(preg_replace('/[\r\n]/', ' ', file_get_contents(MRBS_ROOT . '/images/eye_off.svg')))
?></div>'));
});
<?php // And add the password visibility toggling mechanism ?>
$('.eye svg').on('mousedown', function(e) {
e.preventDefault();
var parent = $(this).parent();
var grandparent = parent.parent();
var input = grandparent.find('input');
var newType = (parent.hasClass('off')) ? 'password' : 'text';
grandparent.find('.eye svg').toggle();
input.attr('type', newType);
});
<?php // De-obfuscate email addresses ?>
$('.contact').each(function() {
var decoded = base64Decode($(this).data('html'));
if (decoded !== false) {
$(this).replaceWith(decoded);
}
});
<?php // Add client-side validation of the file upload size ?>
$('input[type="file"]').on('change input', function(e) {
var maxFileSize = $(this).closest('form').find('[name="MAX_FILE_SIZE"]').val();
var message = '<?php echo get_js_vocab("max_allowed_file_size", ini_get('upload_max_filesize'))?>'
<?php // Check that we know MAX_FILE_SIZE and for browser support ?>
if(maxFileSize && e.target.files && e.target.files.length === 1) {
if (e.target.files[0].size > maxFileSize) {
e.target.setCustomValidity(message);
e.target.reportValidity();
}
else {
e.target.setCustomValidity('');
}
}
});
<?php
// There is a bug in some versions of Safari that means that the value
// of a form input field of type 'email' is not shown when the multiple
// attribute is set. To get round this we convert those fields to type
// 'text', which forces the value to be displayed, and then convert them
// back again to 'email'. See GitHub Issues #3716 and #3717. See also
// https://stackoverflow.com/questions/78403943/safari-issues-with-input-type-email-and-multiple-attribute
// This workaround can be removed once Safari has been fixed.
?>
$('input[type="email"][multiple]').each(function() {
$(this).attr('type', 'text').attr('type', 'email');
});
});
<?php // We define our own page ready event so that we can trigger it after an Ajax load ?>
$(function() {
$(document).trigger('page_ready');
});