包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、JS空集合保护、 会话过期体验优化(403 JSON)、display_errors 关闭、 固定 key 根治 Integrity check failed 等全部改动 注意:config.inc.php/.htaccess/.user.ini 含敏感信息, 通过 .gitignore 排除,勿推送到公开仓库。
511 lines
17 KiB
PHP
511 lines
17 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace MRBS;
|
|
|
|
use MRBS\Intl\FormatterFlatpickr;use MRBS\Intl\IntlDatePatternConverter;
|
|
|
|
require "../defaultincludes.inc";
|
|
|
|
// We use ICU datetime patterns in the config file rather than flatpickr formatting
|
|
// tokens so that we can switch to a different datepicker if necessary, without
|
|
// having to change the config settings.
|
|
function get_date_format() : string
|
|
{
|
|
global $datetime_formats;
|
|
|
|
if (isset($datetime_formats['datepicker']['pattern']))
|
|
{
|
|
try
|
|
{
|
|
$converter = new IntlDatePatternConverter(new FormatterFlatpickr());
|
|
return $converter->convert($datetime_formats['datepicker']['pattern']);
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
// Report the error and fall through to the 'custom' format
|
|
trigger_error($e->getMessage(), E_USER_WARNING);
|
|
}
|
|
}
|
|
|
|
return 'custom';
|
|
}
|
|
|
|
http_headers(array("Content-type: application/x-javascript"),
|
|
60*30); // 30 minute expiry
|
|
?>
|
|
|
|
'use strict';
|
|
|
|
<?php
|
|
// Fix for iOS 13 where the User Agent string has been changed.
|
|
// See https://github.com/flatpickr/flatpickr/issues/1992
|
|
?>
|
|
function iPadMobileFix() {
|
|
return function(instance) {
|
|
return {
|
|
onParseConfig: function() {
|
|
if (instance.isMobile)
|
|
{
|
|
return;
|
|
}
|
|
if (isIos())
|
|
{
|
|
instance.isMobile = true;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
<?php
|
|
// Turn a JavaScript year, month, day (ie with Jan = 0) into an
|
|
// ISO format YYYY-MM-DD date string. Can cope with months
|
|
// outside the range 0..11, but days must be valid.
|
|
?>
|
|
function getISODate(year, month, day)
|
|
{
|
|
<?php // toISOString() converts to UTC, so make the date a UTC date ?>
|
|
var date = new Date(Date.UTC(year, month, day));
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
|
|
|
|
// Given a JavaScript Date object returns a date string in YYYY-MM-DD
|
|
// format. (Note that toISOString() returns a date in UTC time).
|
|
function getLocalISODateString(date)
|
|
{
|
|
var month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
var day = date.getDate().toString().padStart(2, '0');
|
|
var year = date.getFullYear().toString();
|
|
|
|
return [year, month, day].join('-');
|
|
}
|
|
|
|
|
|
|
|
<?php
|
|
// Functions to find the start and end dates of a week and month given a
|
|
// date in YYYY-MM-DD format.
|
|
// weekStarts is the start day of the week (0 for Sunday, 1 for Monday etc.)
|
|
// (Could be implemented by extending the Date class, but extends isn't
|
|
// supported by IE11.)
|
|
?>
|
|
|
|
function weekStart(date, weekStarts) {
|
|
<?php
|
|
// Need to add a time to make sure the date is interpreted as local rather than UTC.
|
|
// "When the time zone offset is absent, date-only forms are interpreted as a UTC time and
|
|
// date-time forms are interpreted as local time. This is due to a historical spec error
|
|
// that was not consistent with ISO 8601 but could not be changed due to web compatibility."
|
|
?>
|
|
var d = new Date(date + "T00:00:00");
|
|
var diff = d.getDay() - weekStarts;
|
|
if (diff < 0)
|
|
{
|
|
diff += 7;
|
|
}
|
|
d.setDate(d.getDate() - diff);
|
|
return getLocalISODateString(d);
|
|
}
|
|
|
|
function weekEnd(date, weekStarts) {
|
|
<?php // Need to add a time to make sure the date is interpreted as local rather than UTC. ?>
|
|
var d = new Date(weekStart(date, weekStarts) + "T00:00:00");
|
|
d.setDate(d.getDate() + 6);
|
|
return getLocalISODateString(d);
|
|
}
|
|
|
|
function monthStart(date) {
|
|
<?php // Need to add a time to make sure the date is interpreted as local rather than UTC. ?>
|
|
var d = new Date(date + "T00:00:00");
|
|
d.setDate(1);
|
|
return getLocalISODateString(d);
|
|
}
|
|
|
|
function monthEnd(date) {
|
|
<?php // Need to add a time to make sure the date is interpreted as local rather than UTC. ?>
|
|
var d = new Date(date + "T00:00:00");
|
|
<?php
|
|
// Set the date to the first of the month, because otherwise we will
|
|
// advance by two months when incrementing the month below if we're at the
|
|
// end of the month and the next month has fewer days than this one.
|
|
?>
|
|
d.setDate(1);
|
|
d.setMonth(d.getMonth() + 1);
|
|
d.setDate(0);
|
|
return getLocalISODateString(d);
|
|
}
|
|
|
|
|
|
// Returns an array of dates in the range startDate..endDate, optionally
|
|
// excluding hidden days.
|
|
function datesInRange(startDate, endDate, excludeHiddenDays) {
|
|
var result=[];
|
|
<?php // Need to add a time to make sure the date is interpreted as local rather than UTC. ?>
|
|
var e=new Date(endDate + "T00:00:00");
|
|
var hiddenDays = [<?php echo implode(',', $hidden_days)?>];
|
|
|
|
<?php // dates can be compared using > and < but not == or === ?>
|
|
for (var d=new Date(startDate + "T00:00:00"); !(d>e); d.setDate(d.getDate()+1))
|
|
{
|
|
if (excludeHiddenDays && (hiddenDays.indexOf(d.getDay()) >= 0))
|
|
{
|
|
continue;
|
|
}
|
|
result.push(getLocalISODateString(d));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
$(document).on('page_ready', function() {
|
|
|
|
const locales = args.langPrefs;
|
|
|
|
<?php
|
|
// Set up datepickers. We convert all inputs of type 'date' into flatpickr
|
|
// datepickers. Note that by default flatpickr will use the native datepickers
|
|
// on mobile devices because they are generally better.
|
|
|
|
// Localise the flatpickr
|
|
// Could use new URLSearchParams(document.currentScript.src); and get the lang parameter from the
|
|
// file's query string, but document.currentScript is not supported by IE (though that probably
|
|
// doesn't matter much anymore).
|
|
if (null !== ($flatpickr_lang_path = Language::getInstance()->getFlatpickrLangPath()))
|
|
{
|
|
// Map the flatpickr lang file onto a flatpickr l10ns property and then localize
|
|
echo 'flatpickr.localize(flatpickr.l10ns["' . Language::getFlatpickrProperty($flatpickr_lang_path) . '"]);';
|
|
}
|
|
|
|
|
|
// Custom date formatter. At the moment, only two format strings are supported:
|
|
//
|
|
// 'custom' The date is formatted in numeric form in the user's preferred locale,
|
|
// as expressed by their browser preferences and subject to any
|
|
// overriding config settings. Note that 'custom' is not supported on
|
|
// IE10, as it requires Intl.DateTimeFormat(), and so IE10 users
|
|
// are given a date in 'Y-m-d' format.
|
|
//
|
|
// everything All other format strings are treated as 'Y-m-d'.
|
|
// else
|
|
?>
|
|
var formatDate = function(dateObj, formatStr) {
|
|
<?php
|
|
// If window.Intl is supported then we can format dates in the user's preferred
|
|
// locale. Otherwise, in practice just IE10, they have to make do with ISO
|
|
// (YYYY-MM-DD) dates.
|
|
?>
|
|
if (window.Intl && (formatStr === 'custom'))
|
|
{
|
|
<?php
|
|
if ($language_debug)
|
|
{
|
|
?>
|
|
console.debug("MRBS language preferences: " + JSON.stringify(locales));
|
|
console.debug("MRBS: locales supported by Intl.DateTimeFormat: " + JSON.stringify(Intl.DateTimeFormat.supportedLocalesOf(locales)));
|
|
<?php
|
|
}
|
|
// Check whether we can use our first choice locale for date and time formatting.
|
|
?>
|
|
if (!Intl.DateTimeFormat.supportedLocalesOf(locales[0]).length)
|
|
{
|
|
console.warn("MRBS: Intl.DateTimeFormat in this browser does not support the '" + locales[0] + "' locale.");
|
|
}
|
|
return (typeof locales === 'undefined') ?
|
|
new Intl.DateTimeFormat().format(dateObj) :
|
|
new Intl.DateTimeFormat(locales).format(dateObj);
|
|
}
|
|
|
|
return getISODate(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate());
|
|
};
|
|
|
|
|
|
var onDayCreate = function(dObj, dStr, fp, dayElem) {
|
|
<?php
|
|
// If the datepicker is used for navigation, and this is a hidden day, and the user is
|
|
// a booking admin, then add a class to the day so that it can be styled differently.
|
|
// If they're not a booking admin, and it's a navigation datepicker, the day will be
|
|
// disabled - see later on in this file.
|
|
?>
|
|
const hiddenDays = [<?php echo implode(',', $hidden_days)?>];
|
|
if (hiddenDays.length &&
|
|
args.isBookAdmin &&
|
|
fp.altInput.classList.contains('navigation') &&
|
|
(hiddenDays.indexOf(dayElem.dateObj.getDay()) >= 0))
|
|
{
|
|
dayElem.classList.add('mrbs-hidden');
|
|
}
|
|
|
|
<?php // And add a class if it's a weekend day ?>
|
|
const weekDays = [<?php echo implode(',', $weekdays)?>];
|
|
if (weekDays.indexOf(dayElem.dateObj.getDay()) < 0) {
|
|
dayElem.classList.add('mrbs-weekend');
|
|
}
|
|
};
|
|
|
|
|
|
<?php
|
|
// Sync all the mini-calendars with this instance of one. In other words
|
|
// make the mini-calendars show sequential months, aligning with this one.
|
|
?>
|
|
function syncCals(instance)
|
|
{
|
|
var thisId = instance.element.attributes.id.nodeValue,
|
|
thisIndex = parseInt(thisId.substring(3), 10),
|
|
currentMonth = parseInt(instance.currentMonth, 10),
|
|
currentYear = parseInt(instance.currentYear, 10);
|
|
|
|
$.each(minicalendars, function(key, value) {
|
|
if (value.element.attributes.id.nodeValue !== thisId)
|
|
{
|
|
var index = parseInt(value.element.attributes.id.nodeValue.substring(3), 10);
|
|
value.jumpToDate(getISODate(currentYear, currentMonth + index - thisIndex, 1));
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
var onMonthChange = function(selectedDates, dateStr, instance) {
|
|
syncCals(instance);
|
|
};
|
|
|
|
var onYearChange = function(selectedDates, dateStr, instance) {
|
|
syncCals(instance);
|
|
};
|
|
|
|
var onMinicalChange = function(selectedDates, dateStr, instance) {
|
|
<?php
|
|
// The order of the query string parameters is important here. It needs to be the
|
|
// same as the order in the Prev and Next navigation links so that the pre-fetched
|
|
// pages can be used when possible.
|
|
?>
|
|
var href = 'index.php';
|
|
href += '?view=' + args.view;
|
|
href += '&page_date=' + dateStr;
|
|
href += '&area=' + args.area;
|
|
href += '&room=' + args.room;
|
|
if (args.site)
|
|
{
|
|
href += '&site=' + encodeURIComponent(args.site);
|
|
}
|
|
<?php
|
|
// Set the new date in the mini-calendar, in order to avoid the previous one
|
|
// still showing as selected.
|
|
// TODO: change the date in the other mini-calendar of the date appears there as well?
|
|
?>
|
|
instance.setDate([selectedDates[0], selectedDates[0]], false);
|
|
updateBody(href); <?php // Update the body via an Ajax call to avoid flickering ?>
|
|
};
|
|
|
|
var lastValidDate = {};
|
|
|
|
<?php
|
|
$date_format = get_date_format();
|
|
?>
|
|
|
|
var config = {
|
|
plugins: [iPadMobileFix()],
|
|
dateFormat: 'Y-m-d',
|
|
altInput: true,
|
|
altFormat: <?php echo "'" . get_date_format() . "'" ?>,
|
|
<?php
|
|
if ($date_format == 'custom')
|
|
{
|
|
echo "formatDate: formatDate,\n";
|
|
}
|
|
?>
|
|
onDayCreate: onDayCreate,
|
|
locale: {firstDayOfWeek: <?php echo $weekstarts ?>},
|
|
onChange: function(selectedDates, dateStr, instance) {
|
|
var element = $(instance.element);
|
|
var submit = element.data('submit');
|
|
var elementName = element.attr('name');
|
|
<?php
|
|
// Flatpickr allows the user to delete the date in the input field, even when allowInput
|
|
// is false, resulting in an empty string, which then causes problems for edit_entry_handler.php.
|
|
// See https://github.com/flatpickr/flatpickr/issues/936 and
|
|
// https://github.com/flatpickr/flatpickr/pull/2252 . At the time of writing the PR has not been
|
|
// merged. To get round this we substitute an empty string for the last known valid date,
|
|
// but only for required inputs.
|
|
?>
|
|
if (dateStr) {
|
|
lastValidDate[elementName] = dateStr;
|
|
}
|
|
else if (element.prop('required') && lastValidDate[elementName]) {
|
|
instance.setDate(lastValidDate[elementName]);
|
|
}
|
|
<?php // Submit will be set for the datepicker in the banner ?>
|
|
if (submit) {
|
|
$('#' + submit).trigger('submit');
|
|
}
|
|
else {
|
|
element.trigger('change');
|
|
}
|
|
},
|
|
onReady: function(selectedDates, dateStr, instance) {
|
|
if (instance.altInput.ariaLabel === null) {
|
|
instance.altInput.ariaLabel = instance.input.ariaLabel;
|
|
}
|
|
if (dateStr) {
|
|
lastValidDate[instance.element.getAttribute('name')] = dateStr;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
<?php
|
|
// Setting weekNumbers causes flatpickr not to use the native datepickers on mobile
|
|
// devices. As these are generally better than flatpickr's, it's probably better
|
|
// to have the native datepicker and do without the week numbers.
|
|
?>
|
|
if (isMobile())
|
|
{
|
|
$('input.flatpickr-input').width('auto');
|
|
}
|
|
else
|
|
{
|
|
<?php // Only display the week number if the MRBS week starts on the first day of the week ?>
|
|
config.weekNumbers = <?php echo ($mincals_week_numbers && ($weekstarts == DateTime::firstDayOfWeek($timezone, Language::getInstance()->getWebLocale()))) ? 'true' : 'false' ?>;
|
|
}
|
|
|
|
let fps = flatpickr('input[type="date"]:not(.navigation)', config);
|
|
// flatpickr will either return a single instance or an array of instances,
|
|
// so convert a single instance to an array so that the iteration works.
|
|
fps = (Array.isArray(fps) ? fps : [fps]);
|
|
fps.forEach(function(fp) {
|
|
<?php
|
|
// For dates that have a data-min attribute, set the minDate.
|
|
?>
|
|
let minDate = $(fp.input).data('min');
|
|
if (minDate)
|
|
{
|
|
fp.set('minDate', minDate);
|
|
// Note that although the flatpickr documentation says this should work on mobiles, it doesn't seem to.
|
|
// So we manually set the 'min' attribute for mobiles.
|
|
$('input[type="hidden"][data-min]').next('.flatpickr-input.flatpickr-mobile').attr('min', minDate);
|
|
}
|
|
});
|
|
|
|
<?php
|
|
// For datepickers used for navigation we need to modify the config to take account
|
|
// of hidden days.
|
|
|
|
// Disable hidden days, unless the user is a booking admin. (If they're a booking
|
|
// admin then they'll still be able to select the date, but it will be given a different
|
|
// class so that it can be styled differently - see code above in this file.)
|
|
if (!empty($hidden_days))
|
|
{
|
|
?>
|
|
if (!args.isBookAdmin)
|
|
{
|
|
config.disable = [
|
|
function (date) {
|
|
return ([<?php echo implode(',', $hidden_days); ?>].indexOf(date.getDay()) >= 0);
|
|
}
|
|
];
|
|
}
|
|
<?php
|
|
}
|
|
?>
|
|
|
|
flatpickr('input[type="date"].navigation', config);
|
|
|
|
<?php
|
|
// Build the mini-calendars, if required.
|
|
if (!empty($display_mincals))
|
|
{
|
|
?>
|
|
if (!isMobile())
|
|
{
|
|
var div = $('.minicalendars');
|
|
if (div.length > 0)
|
|
{
|
|
for (var i = 0; i < 2; i++)
|
|
{
|
|
<?php // Add the 'navigation' class so that the JavaScript knows it can use hidden days ?>
|
|
div.append($('<span class="minicalendar navigation" id="cal' + i + '"></span>'));
|
|
}
|
|
config.inline = true;
|
|
config.onMonthChange = onMonthChange;
|
|
config.onYearChange = onYearChange;
|
|
config.onChange = onMinicalChange;
|
|
<?php
|
|
// Setting a range only works if there are no hidden days: it does not make
|
|
// sense to set a start date of a range on a disabled day.
|
|
if (empty($hidden_days))
|
|
{
|
|
?>
|
|
config.mode = 'range';
|
|
<?php
|
|
}
|
|
?>
|
|
|
|
var minicalendars = flatpickr('span.minicalendar', config);
|
|
|
|
$.each(minicalendars, function (key, value) {
|
|
var startDate, endDate;
|
|
if (args.view === 'month')
|
|
{
|
|
startDate = monthStart(args.pageDate);
|
|
endDate = monthEnd(args.pageDate);
|
|
}
|
|
else if (args.view === 'week')
|
|
{
|
|
startDate = weekStart(args.pageDate, <?php echo $weekstarts?>);
|
|
endDate = weekEnd(args.pageDate, <?php echo $weekstarts?>);
|
|
}
|
|
else
|
|
{
|
|
startDate = args.pageDate;
|
|
endDate = startDate;
|
|
}
|
|
<?php
|
|
if (empty($hidden_days))
|
|
{
|
|
?>
|
|
value.setDate([startDate, endDate]);
|
|
<?php
|
|
}
|
|
else
|
|
{
|
|
// If we've got hidden days then highlight in the mini-calendars those
|
|
// days in the range that are not hidden.
|
|
?>
|
|
value.setDate(datesInRange(startDate, endDate, true));
|
|
<?php
|
|
}
|
|
?>
|
|
value.changeMonth(key);
|
|
});
|
|
|
|
<?php
|
|
// Align the top of the mini-calendars with the top of the navigation bar
|
|
?>
|
|
div.css('margin-top', $('.view_container h2').outerHeight(true) + 'px');
|
|
|
|
<?php
|
|
// Once the calendars are formed then we add the class 'formed' which will
|
|
// bring into play CSS media queries. We need to do this because if we
|
|
// form them when the media queries are operational then they won't get
|
|
// formed if the result of the query is 'display: none', which means that if
|
|
// the window is later widened, for example, they still won't appear when they
|
|
// should do.
|
|
?>
|
|
div.addClass('formed');
|
|
}
|
|
}
|
|
<?php
|
|
}
|
|
|
|
// Only show the main table and navigation once the mini-calendars are in place
|
|
// in order to avoid the screen jiggling about.
|
|
?>
|
|
$('.view_container').removeClass('js_hidden');
|
|
|
|
<?php
|
|
// Show the datepicker in the banner, which has been hidden up until now
|
|
?>
|
|
$('#form_nav').removeClass('js_hidden');
|
|
});
|
|
|