包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
@@ -0,0 +1,985 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
require 'defaultincludes.inc';
|
||||
require_once 'mrbs_sql.inc';
|
||||
require_once 'functions_mail.inc';
|
||||
|
||||
use MRBS\Calendar\CalendarFactory;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
function invalid_date(string $message, bool $is_ajax) : void
|
||||
{
|
||||
if ($is_ajax)
|
||||
{
|
||||
http_response_code(500);
|
||||
// Trigger the error after we have sent the 500 code so that if $debug is set the JavaScript
|
||||
// does not interpret the output as success.
|
||||
trigger_error($message, E_USER_WARNING);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
|
||||
// Check that a room id is set and not the empty string and convert it to an int.
|
||||
function sanitize_room_id($id) : int
|
||||
{
|
||||
if (!isset($id))
|
||||
{
|
||||
throw new Exception("Room id not set");
|
||||
}
|
||||
|
||||
if ($id === '')
|
||||
{
|
||||
throw new Exception("Room id is ''");
|
||||
}
|
||||
|
||||
return intval($id);
|
||||
}
|
||||
|
||||
|
||||
function invalid_booking(string $message) : void
|
||||
{
|
||||
global $view, $view_all, $year, $month, $day, $area, $room;
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room ?? null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
echo "<h1>" . get_vocab('invalid_booking') . "</h1>\n";
|
||||
echo "<p>$message</p>\n";
|
||||
// Print footer and exit
|
||||
print_footer(true);
|
||||
}
|
||||
|
||||
|
||||
$is_ajax = is_ajax();
|
||||
|
||||
if ($is_ajax && !checkAuthorised(this_page(), true))
|
||||
{
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
|
||||
// (1) Check the user is authorised for this page
|
||||
// ---------------------------------------------
|
||||
checkAuthorised(this_page());
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
$mrbs_username = (isset($mrbs_user)) ? $mrbs_user->username : null;
|
||||
|
||||
|
||||
// (2) Get the form variables
|
||||
// --------------------------
|
||||
|
||||
// NOTE: the code on this page assumes that array form variables are passed
|
||||
// as an array of values, rather than an array indexed by value. This is
|
||||
// particularly important for checkbox arrays which should be formed like this:
|
||||
//
|
||||
// <input type="checkbox" name="foo[]" value="n">
|
||||
// <input type="checkbox" name="foo[]" value="m">
|
||||
//
|
||||
// and not like this:
|
||||
//
|
||||
// <input type="checkbox" name="foo[n]" value="1">
|
||||
// <input type="checkbox" name="foo[m]" value="1">
|
||||
|
||||
|
||||
// This page can be called with an Ajax call. In this case it just checks
|
||||
// the validity of a proposed booking and does not make the booking.
|
||||
|
||||
// Get non-standard form variables
|
||||
$form_vars = array(
|
||||
'create_by' => 'string',
|
||||
'name' => 'string',
|
||||
'description' => 'string',
|
||||
'start_seconds' => 'int',
|
||||
'start_date' => 'string',
|
||||
'end_seconds' => 'int',
|
||||
'end_date' => 'string',
|
||||
'all_day' => 'string', // bool, actually
|
||||
'type' => 'string',
|
||||
'rooms' => 'array',
|
||||
'original_room_id' => 'int',
|
||||
'ical_uid' => 'string',
|
||||
'ical_sequence' => 'int',
|
||||
'ical_recur_id' => 'string',
|
||||
'allow_registration' => 'string', // bool, actually
|
||||
'registrant_limit' => 'int',
|
||||
'registrant_limit_enabled' => 'string', // bool, actually
|
||||
'registration_opens_value' => 'int',
|
||||
'registration_opens_units' => 'string',
|
||||
'registration_opens_enabled' => 'string', // bool, actually
|
||||
'registration_closes_value' => 'int',
|
||||
'registration_closes_units' => 'string',
|
||||
'registration_closes_enabled' => 'string', // bool, actually
|
||||
'returl' => 'url_local',
|
||||
'id' => 'int',
|
||||
'rep_id' => 'int',
|
||||
'edit_series' => 'bool',
|
||||
'rep_type' => 'int',
|
||||
'rep_end_date' => 'string',
|
||||
'rep_day' => 'array', // array of bools
|
||||
'rep_interval' => 'int',
|
||||
'month_type' => 'int',
|
||||
'month_absolute' => 'int',
|
||||
'month_relative_ord' => 'string',
|
||||
'month_relative_day' => 'string',
|
||||
'skip' => 'bool',
|
||||
'no_mail' => 'bool',
|
||||
'private' => 'string', // bool, actually
|
||||
'confirmed' => 'string',
|
||||
'back_button' => 'string',
|
||||
'timetohighlight' => 'int',
|
||||
'commit' => 'string'
|
||||
);
|
||||
|
||||
foreach($form_vars as $var => $var_type)
|
||||
{
|
||||
$$var = get_form_var($var, $var_type);
|
||||
|
||||
// Trim the strings and truncate them to the maximum field length
|
||||
if (is_string($$var))
|
||||
{
|
||||
$$var = trim($$var);
|
||||
$$var = truncate($$var, "entry.$var");
|
||||
}
|
||||
}
|
||||
|
||||
// Provide a default for $rep_interval (it could be null in an Ajax post request
|
||||
// if the user has an empty string in the input).
|
||||
if (!isset($rep_interval))
|
||||
{
|
||||
$rep_interval = 1;
|
||||
}
|
||||
|
||||
// Sanitize the room ids
|
||||
$rooms = array_map(__NAMESPACE__ . '\sanitize_room_id', $rooms);
|
||||
|
||||
// Convert the registration opens and closes times into seconds
|
||||
if (isset($registration_opens_value) && isset($registration_opens_units))
|
||||
{
|
||||
$registration_opens = $registration_opens_value;
|
||||
fromTimeString($registration_opens, $registration_opens_units);
|
||||
$registration_opens = constrain_int($registration_opens, 4);
|
||||
}
|
||||
|
||||
if (isset($registration_closes_value) && isset($registration_closes_units))
|
||||
{
|
||||
$registration_closes = $registration_closes_value;
|
||||
fromTimeString($registration_closes, $registration_closes_units);
|
||||
$registration_closes = constrain_int($registration_closes, 4);
|
||||
}
|
||||
|
||||
if (!$is_ajax)
|
||||
{
|
||||
// Convert the database booleans (the custom field booleans are done later)
|
||||
foreach (['allow_registration', 'registrant_limit_enabled', 'registration_opens_enabled', 'registration_closes_enabled'] as $var)
|
||||
{
|
||||
$$var = ($$var) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If they're not an admin and multi-day bookings are not allowed, then
|
||||
// set the end date to the start date
|
||||
if (!is_book_admin($rooms) && $auth['only_admin_can_book_multiday'])
|
||||
{
|
||||
$end_date = $start_date;
|
||||
}
|
||||
|
||||
if (false === ($start_date_split = split_iso_date($start_date)))
|
||||
{
|
||||
invalid_date("Invalid start_date '$start_date'", $is_ajax);
|
||||
}
|
||||
list($start_year, $start_month, $start_day) = $start_date_split;
|
||||
|
||||
if (false === ($end_date_split = split_iso_date($end_date)))
|
||||
{
|
||||
invalid_date("Invalid end_date '$end_date'", $is_ajax);
|
||||
}
|
||||
list($end_year, $end_month, $end_day) = $end_date_split;
|
||||
|
||||
|
||||
// BACK: we didn't really want to be here - send them to the returl
|
||||
if (!empty($back_button))
|
||||
{
|
||||
if (empty($returl))
|
||||
{
|
||||
$returl = "index.php";
|
||||
}
|
||||
location_header($returl);
|
||||
}
|
||||
|
||||
// Get custom form variables
|
||||
$custom_fields = array();
|
||||
|
||||
// Get the information about the fields in the entry table
|
||||
$fields = db()->field_info(_tbl('entry'));
|
||||
|
||||
foreach($fields as $field)
|
||||
{
|
||||
if (!in_array($field['name'], $standard_fields['entry']))
|
||||
{
|
||||
$f_type = get_form_var_type($field);
|
||||
$var = VAR_PREFIX . $field['name'];
|
||||
$custom_fields[$field['name']] = get_form_var($var, $f_type);
|
||||
|
||||
// Trim any strings and truncate them to the maximum field length
|
||||
if (is_string($custom_fields[$field['name']]) && ($field['nature'] != 'decimal'))
|
||||
{
|
||||
$custom_fields[$field['name']] = trim($custom_fields[$field['name']]);
|
||||
$custom_fields[$field['name']] = truncate($custom_fields[$field['name']], 'entry.' . $field['name']);
|
||||
}
|
||||
|
||||
// For certain nullable data types convert empty strings into null values, to prevent an SQL error
|
||||
if (in_array($field['nature'], ['decimal', 'timestamp']))
|
||||
{
|
||||
if ($field['is_nullable'] && (!isset($custom_fields[$field['name']]) || ($custom_fields[$field['name']] === '')))
|
||||
{
|
||||
$custom_fields[$field['name']] = null;
|
||||
}
|
||||
// TODO: Validate that the fields are valid for the SQL input before they trigger an SQL error?
|
||||
// TODO: Rewrite all of this so that there is common form validation and processing for all forms.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (3) Clean up the form variables
|
||||
// -------------------------------
|
||||
|
||||
// Form validation checks. Normally checked for client side.
|
||||
|
||||
// Validate the create_by variable, checking that it's the current user, unless the
|
||||
// user is an admin and the booking is being edited or it's a new booking and we allow
|
||||
// admins to make bookings on behalf of others.
|
||||
//
|
||||
// Only carry out this check if it's not an Ajax request. If it is an Ajax request then
|
||||
// $create_by isn't set yet, but a getWritable check will be done later,
|
||||
if (!$is_ajax)
|
||||
{
|
||||
if (!isset($create_by))
|
||||
{
|
||||
// Shouldn't happen, unless something's gone wrong with the form or the POST request.
|
||||
throw new Exception('$create_by not set');
|
||||
}
|
||||
if (!is_book_admin($rooms) || (!isset($id) && $auth['admin_can_only_book_for_self']))
|
||||
{
|
||||
if ($create_by !== $mrbs_username)
|
||||
{
|
||||
$message = "Attempt made by user '$mrbs_username' to make a booking in the name of '$create_by'";
|
||||
trigger_error($message, E_USER_NOTICE);
|
||||
$create_by = $mrbs_username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($rooms))
|
||||
{
|
||||
if (!$is_ajax)
|
||||
{
|
||||
invalid_booking(get_vocab('no_rooms_selected'));
|
||||
}
|
||||
if ($commit)
|
||||
{
|
||||
throw new \Exception('No rooms specified');
|
||||
}
|
||||
// If this is an Ajax request and we're not committing the booking, ie we are just
|
||||
// checking for conflicts, then it's perfectly possible to get here without any rooms
|
||||
// being selected on the form (just deselect the room on the form with Ctrl Click). So
|
||||
// in this case just return a null response.
|
||||
http_headers(array("Content-Type: application/json"));
|
||||
echo json_encode(null);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Make sure the area corresponds to the room that is being booked
|
||||
$area = get_area($rooms[0]);
|
||||
get_area_settings($area); // Update the area settings
|
||||
|
||||
// and that $room is in $area
|
||||
if (get_area($room) != $area)
|
||||
{
|
||||
$room = get_default_room($area);
|
||||
}
|
||||
|
||||
// Don't bother with these checks if this is an Ajax request.
|
||||
if (!$is_ajax)
|
||||
{
|
||||
if (!isset($name) || ($name === ''))
|
||||
{
|
||||
invalid_booking(get_vocab('must_set_description'));
|
||||
}
|
||||
|
||||
if (($rep_type != RepeatRule::NONE) && ($rep_interval < 1))
|
||||
{
|
||||
invalid_booking(get_vocab('invalid_rep_interval'));
|
||||
}
|
||||
|
||||
// Check that we've got the mandatory fields
|
||||
if (!empty($is_mandatory_field))
|
||||
{
|
||||
foreach ($is_mandatory_field as $full_field => $value)
|
||||
{
|
||||
if (is_mandatory_field($full_field, $area))
|
||||
{
|
||||
$field = preg_replace('/^entry\./', '', $full_field);
|
||||
if ((in_array($field, $standard_fields['entry']) && ($$field === '')) ||
|
||||
(array_key_exists($field, $custom_fields) && ($custom_fields[$field] === '')))
|
||||
{
|
||||
invalid_booking(get_vocab('missing_mandatory_field') . ' "' .
|
||||
get_loc_field_name(_tbl('entry'), $field) . '"');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($type))
|
||||
{
|
||||
$type = $default_type;
|
||||
}
|
||||
|
||||
// Check that the type is allowed
|
||||
if (!is_book_admin($rooms) && isset($auth['admin_only_types']) && in_array($type, $auth['admin_only_types']))
|
||||
{
|
||||
invalid_booking(get_vocab('type_reserved_for_admins', get_type_vocab($type)));
|
||||
}
|
||||
|
||||
if (isset($month_relative_ord) && isset($month_relative_day))
|
||||
{
|
||||
$month_relative = $month_relative_ord . $month_relative_day;
|
||||
}
|
||||
|
||||
// Handle private bookings.
|
||||
// If the area settings allow users to make private bookings then use the value from
|
||||
// the form, unless the privacy status is forced and the user is not an admin (admins
|
||||
// are allowed to make public bookings if they want, even if the status is forced).
|
||||
// Otherwise the booking is not private, unless the status is forced, in which case
|
||||
// the default applies, whether or not the user is an admin.
|
||||
if ($private_enabled)
|
||||
{
|
||||
$is_private = (!is_book_admin() && $private_mandatory) ? $private_default : (bool) $private;
|
||||
}
|
||||
else
|
||||
{
|
||||
$is_private = ($private_mandatory) ? $private_default : false;
|
||||
}
|
||||
|
||||
// Check that they really are allowed to set $no_mail;
|
||||
if ($no_mail)
|
||||
{
|
||||
if (!$mail_settings['allow_no_mail'] &&
|
||||
(!is_book_admin($rooms) || !$mail_settings['allow_admins_no_mail']))
|
||||
{
|
||||
$no_mail = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an Ajax request and we're being asked to commit the booking, then
|
||||
// we'll only have been supplied with parameters that need to be changed. Fill in
|
||||
// the rest from the existing booking information.
|
||||
// Note: we assume that
|
||||
// (1) this is not a series (we can't cope with them yet)
|
||||
// (2) we always get passed start_seconds and end_seconds in the Ajax data
|
||||
if ($is_ajax && $commit)
|
||||
{
|
||||
$old_booking = get_booking_info($id, false);
|
||||
|
||||
foreach ($form_vars as $var => $var_type)
|
||||
{
|
||||
if (!isset($$var) || (($var_type == 'array') && empty($$var)))
|
||||
{
|
||||
switch ($var)
|
||||
{
|
||||
case 'rep_type':
|
||||
// If it's a series we're just going to change this entry
|
||||
$$var = RepeatRule::NONE;
|
||||
break;
|
||||
case 'rooms':
|
||||
$rooms = array($old_booking['room_id']);
|
||||
break;
|
||||
case 'original_room_id':
|
||||
$$var = $old_booking['room_id'];
|
||||
break;
|
||||
case 'private':
|
||||
$$var = $old_booking['private'];
|
||||
break;
|
||||
case 'confirmed':
|
||||
$$var = !$old_booking['tentative'];
|
||||
break;
|
||||
// In the calculation of $start_seconds and $end_seconds below we need to take
|
||||
// care of the case when 0000 on the day in question is across a DST boundary
|
||||
// from the current time, ie the days on which DST starts and ends.
|
||||
case 'start_seconds':
|
||||
$date = getdate($old_booking['start_time']);
|
||||
$start_year = (int) $date['year'];
|
||||
$start_month = (int) $date['mon'];
|
||||
$start_day = (int) $date['mday'];
|
||||
$start_daystart = mktime(0, 0, 0, $start_month, $start_day, $start_year);
|
||||
$old_start = $old_booking['start_time'];
|
||||
$start_seconds = $old_start - $start_daystart;
|
||||
$start_seconds -= cross_dst($start_daystart, $old_start);
|
||||
break;
|
||||
case 'end_seconds':
|
||||
$date = getdate($old_booking['end_time']);
|
||||
$end_year = (int) $date['year'];
|
||||
$end_month = (int) $date['mon'];
|
||||
$end_day = (int) $date['mday'];
|
||||
$end_daystart = mktime(0, 0, 0, $end_month, $end_day, $end_year);
|
||||
$old_end = $old_booking['end_time'];
|
||||
$end_seconds = $old_end - $end_daystart;
|
||||
$end_seconds -= cross_dst($end_daystart, $old_end);
|
||||
// When using periods end_seconds is actually the start of the last period
|
||||
if ($enable_periods)
|
||||
{
|
||||
$end_seconds -= 60;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (array_key_exists($var, $old_booking))
|
||||
{
|
||||
$$var = $old_booking[$var];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now the custom fields
|
||||
$custom_fields = array();
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
if (!in_array($field['name'], $standard_fields['entry']))
|
||||
{
|
||||
$custom_fields[$field['name']] = $old_booking[$field['name']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// When All Day is checked, $start_seconds and $end_seconds are disabled and so won't
|
||||
// get passed through by the form. We therefore need to set them.
|
||||
if (!empty($all_day))
|
||||
{
|
||||
if ($enable_periods)
|
||||
{
|
||||
$start_seconds = 12 * SECONDS_PER_HOUR;
|
||||
// This is actually the start of the last period, which is what the form would
|
||||
// have returned. It will get corrected in a moment.
|
||||
$end_seconds = $start_seconds + ((count($periods) - 1) * 60);
|
||||
}
|
||||
else
|
||||
{
|
||||
$start_seconds = (($morningstarts * 60) + $morningstarts_minutes) * 60;
|
||||
$end_seconds = (($eveningends * 60) + $eveningends_minutes) *60;
|
||||
$end_seconds += $resolution; // We want the end of the last slot, not the beginning
|
||||
if ($end_seconds <= $start_seconds)
|
||||
{
|
||||
$end_seconds += SECONDS_PER_DAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're operating on a booking day that stretches past midnight, it's more convenient
|
||||
// for the sections past midnight to be shown as being on the day before. That way the
|
||||
// $returl will end up taking us back to the day we started on
|
||||
if (day_past_midnight())
|
||||
{
|
||||
$end_last = (((($eveningends * 60) + $eveningends_minutes) *60) + $resolution) % SECONDS_PER_DAY;
|
||||
if ($start_seconds < $end_last)
|
||||
{
|
||||
$start_seconds += SECONDS_PER_DAY;
|
||||
$day_before = getdate(mktime(0, 0, 0, $start_month, $start_day-1, $start_year));
|
||||
$start_day = (int) $day_before['mday'];
|
||||
$start_month = (int) $day_before['mon'];
|
||||
$start_year = (int) $day_before['year'];
|
||||
}
|
||||
}
|
||||
|
||||
$target_rooms = $rooms;
|
||||
|
||||
// Check that the user has permission to create/edit an entry for this room.
|
||||
// Get the id of the room that we are creating/editing
|
||||
if (isset($id))
|
||||
{
|
||||
// Editing an existing booking: get the room_id from the database (you can't
|
||||
// get it from $rooms because they are the new rooms)
|
||||
$sql = "SELECT room_id
|
||||
FROM " . _tbl('entry') . "
|
||||
WHERE id=?
|
||||
LIMIT 1";
|
||||
$existing_room = db()->query1($sql, array($id));
|
||||
if ($existing_room < 0)
|
||||
{
|
||||
// Ideally we should give more feedback to the user when this happens, or
|
||||
// even lock the entry once a user starts to edit it.
|
||||
$message = "Tried to edit an entry that no longer exists - probably because " .
|
||||
"somebody else has deleted it in the meantime.";
|
||||
trigger_error($message, E_USER_NOTICE);
|
||||
location_header($returl);
|
||||
}
|
||||
$target_rooms[] = $existing_room;
|
||||
$target_rooms = array_unique($target_rooms);
|
||||
}
|
||||
|
||||
// Must have write access to at least one of the rooms
|
||||
if (!getWritable($create_by, $target_rooms, false))
|
||||
{
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, $room ?? null);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
if ($enable_periods)
|
||||
{
|
||||
$resolution = 60;
|
||||
}
|
||||
|
||||
// Now work out the start and times
|
||||
$start_time = mktime(0, 0, $start_seconds, $start_month, $start_day, $start_year);
|
||||
$end_time = mktime(0, 0, $end_seconds, $end_month, $end_day, $end_year);
|
||||
|
||||
// If we're using periods then the endtime we've been returned by the form is actually
|
||||
// the beginning of the last period in the booking (it's more intuitive for users this way)
|
||||
// so we need to add on 60 seconds (1 period)
|
||||
if ($enable_periods)
|
||||
{
|
||||
$end_time = $end_time + 60;
|
||||
}
|
||||
|
||||
// Round down the starttime and round up the endtime to the nearest slot boundaries
|
||||
// (This step is probably unnecessary now that MRBS always returns times aligned
|
||||
// on slot boundaries, but is left in for good measure).
|
||||
$start_first_slot = get_start_first_slot($start_month, $start_day, $start_year);
|
||||
$start_time = round_t_down($start_time, $resolution, $start_first_slot);
|
||||
$start_first_slot = get_start_first_slot($end_month, $end_day, $end_year);
|
||||
$end_time = round_t_up($end_time, $resolution, $start_first_slot);
|
||||
|
||||
// If they asked for 0 minutes, and even after the rounding the slot length is still
|
||||
// 0 minutes, push that up to 1 resolution unit.
|
||||
if ($end_time == $start_time)
|
||||
{
|
||||
$end_time += $resolution;
|
||||
}
|
||||
|
||||
if (!isset($rep_type))
|
||||
{
|
||||
$rep_type = RepeatRule::NONE;
|
||||
}
|
||||
|
||||
if (!isset($rep_day))
|
||||
{
|
||||
$rep_day = array();
|
||||
}
|
||||
|
||||
// Get the repeat details
|
||||
$repeat_rule = new RepeatRule();
|
||||
$repeat_rule->setType($rep_type ?? RepeatRule::NONE);
|
||||
|
||||
if ($repeat_rule->getType() != RepeatRule::NONE)
|
||||
{
|
||||
$repeat_rule->setInterval($rep_interval);
|
||||
if ($repeat_rule->getType() == RepeatRule::MONTHLY)
|
||||
{
|
||||
$repeat_rule->setMonthlyType($month_type);
|
||||
if ($repeat_rule->getMonthlyType() == RepeatRule::MONTHLY_ABSOLUTE)
|
||||
{
|
||||
$repeat_rule->setMonthlyAbsolute($month_absolute);
|
||||
}
|
||||
else
|
||||
{
|
||||
$repeat_rule->setMonthlyRelative($month_relative);
|
||||
}
|
||||
}
|
||||
if (isset($rep_end_date))
|
||||
{
|
||||
$repeat_end_date = DateTime::createFromFormat(DateTime::ISO8601_DATE, $rep_end_date);
|
||||
if ($repeat_end_date === false)
|
||||
{
|
||||
throw new Exception("Could not create repeat end date");
|
||||
}
|
||||
$repeat_end_date->setTime(intval($start_seconds/SECONDS_PER_HOUR), intval(($start_seconds%SECONDS_PER_HOUR)/60));
|
||||
$repeat_rule->setEndDate($repeat_end_date);
|
||||
}
|
||||
|
||||
if ($repeat_rule->getType() == RepeatRule::WEEKLY)
|
||||
{
|
||||
// If no repeat day has been set, then set a default repeat day
|
||||
// as the day of the week of the start of the period
|
||||
$repeat_rule->setDays ((count($rep_day) > 0) ? $rep_day : array(date('w', $start_time)));
|
||||
}
|
||||
|
||||
// Make sure that the starttime coincides with a repeat day. In
|
||||
// other words make sure that the first starttime defines an actual
|
||||
// entry. We need to do this because if we are going to construct an iCalendar
|
||||
// object, RFC 5545 demands that the start time is the first event of
|
||||
// a series. ['The "DTSTART" property for a "VEVENT" specifies the inclusive
|
||||
// start of the event. For recurring events, it also specifies the very first
|
||||
// instance in the recurrence set.']
|
||||
|
||||
// Get the first entry in the series and make that the start time
|
||||
$reps = $repeat_rule->getRepeatStartTimes($start_time, 1);
|
||||
|
||||
if (count($reps) > 0)
|
||||
{
|
||||
$duration = $end_time - $start_time;
|
||||
$duration -= cross_dst($start_time, $end_time);
|
||||
$start_time = $reps[0];
|
||||
$end_time = $start_time + $duration;
|
||||
$start_day = (int) date('j', $start_time);
|
||||
$start_month = (int) date('n', $start_time);
|
||||
$start_year = (int) date('Y', $start_time);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're committing this booking, get the start day/month/year and
|
||||
// make them the current day/month/year
|
||||
if (!$is_ajax || $commit)
|
||||
{
|
||||
$day = $start_day;
|
||||
$month = $start_month;
|
||||
$year = $start_year;
|
||||
}
|
||||
|
||||
// Set up the return URL. As the user has tried to book a particular room and a particular
|
||||
// day, we must consider these to be the new "sticky room" and "sticky day", so modify the
|
||||
// return URL accordingly.
|
||||
|
||||
// First get the return URL basename, having stripped off the old query string
|
||||
// (1) It's possible that $returl could be empty, for example if edit_entry.php had been called
|
||||
// direct, perhaps if the user has it set as a bookmark
|
||||
// (2) Avoid an endless loop. It shouldn't happen, but just in case ...
|
||||
// (3) If you've come from search, you probably don't want to go back there (and if you did we'd
|
||||
// have to preserve the search parameter in the query string)
|
||||
if (isset($returl) && ($returl !== ''))
|
||||
{
|
||||
$returl = parse_url($returl);
|
||||
if ($returl !== false)
|
||||
{
|
||||
if (isset($returl['query']))
|
||||
{
|
||||
parse_str($returl['query'], $query_vars);
|
||||
}
|
||||
$view = $query_vars['view'] ?? $default_view;
|
||||
$view_all = $query_vars['view_all'] ?? (($default_view_all) ? 1 : 0);
|
||||
$returl = explode('/', $returl['path']);
|
||||
$returl = end($returl);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($returl) ||
|
||||
in_array($returl, array('edit_entry.php',
|
||||
'edit_entry_handler.php',
|
||||
'search.php')))
|
||||
{
|
||||
$returl = 'index.php';
|
||||
}
|
||||
|
||||
// If we haven't been given a sensible date then get out of here and don't try and make a booking
|
||||
if (!isset($start_day) || !isset($start_month) || !isset($start_year) || !checkdate($start_month, $start_day, $start_year))
|
||||
{
|
||||
location_header($returl);
|
||||
}
|
||||
|
||||
// If the old sticky room is one of the rooms requested for booking, then don't change the sticky room.
|
||||
// Otherwise change the sticky room to be one of the new rooms.
|
||||
if (!in_array($room, $rooms))
|
||||
{
|
||||
$room = $rooms[0];
|
||||
}
|
||||
// Find the corresponding area
|
||||
$area = get_area($room);
|
||||
if (empty($area))
|
||||
{
|
||||
// This shouldn't happen.
|
||||
// TODO: Get rid of the area parameter from the query string, and just use the room parameter, as given the room
|
||||
// TODO: the area is redundant.
|
||||
trigger_error("Room with id $room doesn't exist.", E_USER_WARNING);
|
||||
}
|
||||
|
||||
// Now construct the new query string
|
||||
$vars = [
|
||||
'view' => $view ?? $default_view,
|
||||
'view_all' => $view_all ?? $default_view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room
|
||||
];
|
||||
|
||||
// If we're going back to the index page then add any scroll positions to the
|
||||
// query string so that the JavaScript can scroll back to the same position.
|
||||
if ('index.php' == basename(parse_url($returl, PHP_URL_PATH)))
|
||||
{
|
||||
foreach (['top', 'left'] as $var)
|
||||
{
|
||||
$$var = get_form_var($var, 'string');
|
||||
if (isset($$var))
|
||||
{
|
||||
$vars[$var] = $$var;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$returl .= '?' . http_build_query($vars, '', '&');
|
||||
|
||||
|
||||
// Check to see whether this is a repeat booking and if so, whether the user
|
||||
// is allowed to make/edit repeat bookings. (The edit_entry form should
|
||||
// prevent you ever getting here, but this check is here as a safeguard in
|
||||
// case someone has spoofed the HTML)
|
||||
if (isset($rep_type) && ($rep_type != RepeatRule::NONE) &&
|
||||
!is_book_admin($rooms) &&
|
||||
!empty($auth['only_admin_can_book_repeat']))
|
||||
{
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, $room ?? null);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// (4) Assemble the booking data
|
||||
// -----------------------------
|
||||
|
||||
// Assemble an array of bookings, one for each room
|
||||
$bookings = array();
|
||||
foreach ($rooms as $room_id)
|
||||
{
|
||||
// Ignore rooms for which the user doesn't have write access
|
||||
if (!getWritable($create_by, $room_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking = array();
|
||||
$booking['create_by'] = $create_by;
|
||||
$booking['modified_by'] = (isset($id)) ? $mrbs_username : '';
|
||||
$booking['name'] = $name;
|
||||
$booking['type'] = $type;
|
||||
$booking['description'] = $description;
|
||||
$booking['room_id'] = $room_id;
|
||||
$booking['start_time'] = $start_time;
|
||||
$booking['end_time'] = $end_time;
|
||||
$booking['ical_uid'] = $ical_uid;
|
||||
$booking['ical_sequence'] = $ical_sequence;
|
||||
$booking['ical_recur_id'] = $ical_recur_id;
|
||||
$booking['allow_registration'] = $allow_registration;
|
||||
$booking['registrant_limit'] = $registrant_limit;
|
||||
$booking['registrant_limit_enabled'] = $registrant_limit_enabled;
|
||||
$booking['registration_opens'] = (isset($registration_opens)) ? $registration_opens : null;
|
||||
$booking['registration_opens_enabled'] = $registration_opens_enabled;
|
||||
$booking['registration_closes'] = (isset($registration_closes)) ? $registration_closes : null;
|
||||
$booking['registration_closes_enabled'] = $registration_closes_enabled;
|
||||
$booking['repeat_rule'] = $repeat_rule;
|
||||
|
||||
// Do the custom fields
|
||||
foreach ($custom_fields as $key => $value)
|
||||
{
|
||||
$booking[$key] = $value;
|
||||
}
|
||||
|
||||
// Set the various statuses as appropriate
|
||||
// (Note: the statuses fields are the only ones that can differ by room)
|
||||
|
||||
// Privacy status
|
||||
$booking['private'] = (bool) $is_private;
|
||||
|
||||
// If we are using booking approvals then we need to work out whether the
|
||||
// status of this booking is approved. If the user is allowed to approve
|
||||
// bookings for this room, then the status will be approved, since they are
|
||||
// in effect immediately approving their own booking. Otherwise the booking
|
||||
// will need to approved.
|
||||
$booking['awaiting_approval'] = ($approval_enabled && !is_book_admin($room_id));
|
||||
|
||||
// Confirmation status
|
||||
$booking['tentative'] = ($confirmation_enabled && !$confirmed);
|
||||
|
||||
$bookings[] = $booking;
|
||||
}
|
||||
|
||||
$just_check = $is_ajax && !$commit;
|
||||
$this_id = (isset($id)) ? $id : null;
|
||||
$send_mail = !$no_mail && need_to_send_mail();
|
||||
|
||||
try
|
||||
{
|
||||
// Wrap the editing process in a transaction, because we'll want to roll back the edit if the
|
||||
// deletion of the old booking fails. This could happen, for example, if
|
||||
// (a) somebody else has already edited the booking and the original booking no longer exists; or
|
||||
// (b) if there's some other problem, eg the database user hasn't been granted DELETE rights, in which
|
||||
// case we would be left with two overlapping bookings.
|
||||
db()->begin();
|
||||
$transaction_ok = true;
|
||||
$result = mrbsMakeBookings($bookings, $this_id, $just_check, $skip, $original_room_id, $send_mail, $edit_series);
|
||||
|
||||
// If we weren't just checking and this was a successful booking and
|
||||
// we were editing an existing booking, then delete the old booking
|
||||
if (!$just_check && $result['valid_booking'] && isset($id))
|
||||
{
|
||||
$transaction_ok = mrbsDelEntry($id, $edit_series, true);
|
||||
}
|
||||
|
||||
if ($transaction_ok)
|
||||
{
|
||||
db()->commit();
|
||||
}
|
||||
else
|
||||
{
|
||||
db()->rollback();
|
||||
trigger_error('Edit failed.', E_USER_WARNING);
|
||||
}
|
||||
|
||||
// If this is an Ajax request, output the result and finish
|
||||
if ($is_ajax)
|
||||
{
|
||||
if ($commit)
|
||||
{
|
||||
// Generate the new HTML
|
||||
$calendar = CalendarFactory::create($view, $view_all, $year, $month, $day, $area, $room, $timetohighlight);
|
||||
$result['table_innerhtml'] = $calendar->innerHTML();
|
||||
}
|
||||
http_headers(array("Content-Type: application/json"));
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
if ($is_ajax)
|
||||
{
|
||||
trigger_error('Caught exception: ' . $e->getMessage(), E_USER_WARNING);
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Everything was OK. Go back to where we came from
|
||||
if ($result['valid_booking'])
|
||||
{
|
||||
location_header($returl);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room ?? null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
echo "<h2>" . get_vocab("sched_conflict") . "</h2>\n";
|
||||
if (!empty($result['violations']['errors']))
|
||||
{
|
||||
echo "<p>\n";
|
||||
echo get_vocab("rules_broken") . "\n";
|
||||
echo "</p>\n";
|
||||
echo "<ul>\n";
|
||||
foreach ($result['violations']['errors'] as $rule)
|
||||
{
|
||||
echo "<li>$rule</li>\n";
|
||||
}
|
||||
echo "</ul>\n";
|
||||
}
|
||||
if (!empty($result['conflicts']))
|
||||
{
|
||||
echo "<p>\n";
|
||||
echo get_vocab("conflict") . "\n";
|
||||
echo "</p>\n";
|
||||
echo "<ul>\n";
|
||||
foreach ($result['conflicts'] as $conflict)
|
||||
{
|
||||
echo "<li>$conflict</li>\n";
|
||||
}
|
||||
echo "</ul>\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "<div id=\"submit_buttons\">\n";
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$form->setAttributes(array('action' => multisite(this_page())));
|
||||
|
||||
// Back button
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array(
|
||||
'formaction' => multisite('edit_entry.php'),
|
||||
'name' => 'back_button',
|
||||
'value' => get_vocab('back')
|
||||
));
|
||||
$form->addElement($submit);
|
||||
|
||||
// Skip and Book button (to book the entries that don't conflict)
|
||||
// Only show this button if there were no policies broken and it's a series
|
||||
if (empty($result['violations']['errors']) &&
|
||||
isset($rep_type) && ($rep_type != RepeatRule::NONE))
|
||||
{
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array(
|
||||
'value' => get_vocab('skip_and_book'),
|
||||
'title' => get_vocab('skip_and_book_note')
|
||||
));
|
||||
$form->addElement($submit);
|
||||
// Force a skip next time round
|
||||
$skip = true;
|
||||
}
|
||||
|
||||
// Put the booking data in as hidden inputs
|
||||
// First the ordinary fields
|
||||
foreach ($form_vars as $var => $var_type)
|
||||
{
|
||||
if ($var_type == 'array')
|
||||
{
|
||||
// See the comment at the top of the page about array formats
|
||||
foreach ($$var as $value)
|
||||
{
|
||||
if (isset($value))
|
||||
{
|
||||
$form->addHiddenInput("{$var}[]", $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (isset($$var))
|
||||
{
|
||||
$form->addHiddenInput($var, $$var);
|
||||
}
|
||||
}
|
||||
// Then the custom fields
|
||||
foreach($fields as $field)
|
||||
{
|
||||
if (array_key_exists($field['name'], $custom_fields) && isset($custom_fields[$field['name']]))
|
||||
{
|
||||
$form->addHiddenInput(VAR_PREFIX . $field['name'], $custom_fields[$field['name']]);
|
||||
}
|
||||
}
|
||||
|
||||
$form->render();
|
||||
|
||||
echo "</div>\n";
|
||||
|
||||
print_footer();
|
||||
Reference in New Issue
Block a user