Docker image / push (push) Canceled after 0s
包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
486 lines
18 KiB
PHP
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');
|
|
});
|