Docker image / push (push) Canceled after 0s
包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
1316 lines
40 KiB
PHP
1316 lines
40 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
namespace MRBS;
|
|
|
|
use MRBS\Form\ElementFieldset;
|
|
use MRBS\Form\ElementInputHidden;
|
|
use MRBS\Form\FieldInputCheckbox;
|
|
use MRBS\Form\FieldInputCheckboxGroup;
|
|
use MRBS\Form\FieldInputFile;
|
|
use MRBS\Form\FieldInputRadioGroup;
|
|
use MRBS\Form\FieldInputSubmit;
|
|
use MRBS\Form\FieldInputText;
|
|
use MRBS\Form\FieldInputUrl;
|
|
use MRBS\Form\FieldSelect;
|
|
use MRBS\Form\Form;
|
|
use MRBS\ICalendar\ComponentFactory;
|
|
use MRBS\ICalendar\Event;
|
|
use MRBS\ICalendar\Property;
|
|
use MRBS\ICalendar\RFC5545Exception;
|
|
use ReflectionClass;
|
|
use ZipArchive;
|
|
|
|
|
|
require "defaultincludes.inc";
|
|
require_once "mrbs_sql.inc";
|
|
|
|
define('IMPORT_CREATOR_EMAIL', '0');
|
|
define('IMPORT_CREATOR_USERNAME', '1');
|
|
|
|
$wrapper_mime_types = array('file' => 'text/calendar',
|
|
'zip' => 'application/zip',
|
|
'compress.zlib' => 'application/x-gzip',
|
|
'compress.bzip2' => 'application/x-bzip2');
|
|
|
|
$wrapper_descriptions = array('file' => get_vocab('import_text_file'),
|
|
'zip' => get_vocab('import_zip'),
|
|
'compress.zlib' => get_vocab('import_gzip'),
|
|
'compress.bzip2' => get_vocab('import_bzip2'));
|
|
|
|
// Get the available compression wrappers that we can use.
|
|
// Returns an array
|
|
function get_compression_wrappers() : array
|
|
{
|
|
$result = array();
|
|
if (function_exists('stream_get_wrappers'))
|
|
{
|
|
$wrappers = stream_get_wrappers();
|
|
foreach ($wrappers as $wrapper)
|
|
{
|
|
if ((($wrapper == 'zip') && class_exists('ZipArchive')) ||
|
|
(mb_strpos($wrapper, 'compress.') === 0))
|
|
{
|
|
$result[] = $wrapper;
|
|
}
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a username from the ORGANIZER property.
|
|
*
|
|
* @param string $import_creator If set to IMPORT_CREATOR_USERNAME, then the X-MRBS-USERNAME is used. If that
|
|
* parameter doesn't exist, or if `$import_creator` is set to IMPORT_CREATOR_EMAIL, then MRBS will try to get the
|
|
* username from the email address. If that fails, then the current user's username is used, and failing that the
|
|
* email address in the property.
|
|
*/
|
|
function get_create_by(Property $organizer, string $import_creator) : string
|
|
{
|
|
switch ($import_creator)
|
|
{
|
|
/** @noinspection PhpMissingBreakStatementInspection */
|
|
case IMPORT_CREATOR_USERNAME:
|
|
$usernames = $organizer->getParamValues('X-MRBS-USERNAME');
|
|
if (count($usernames) > 0)
|
|
{
|
|
return $usernames[0];
|
|
}
|
|
// If there's no username parameter, then try to get the username from the email address.
|
|
// Fall through
|
|
|
|
case IMPORT_CREATOR_EMAIL:
|
|
// Get the email address. Stripping off the 'mailto' is a very simplistic
|
|
// method. It will work in the majority of cases, but this needs to be improved
|
|
$email = preg_replace('/^mailto:/', '', $organizer->getValues()[0]);
|
|
if (null === ($result = auth()->getUsernameByEmail($email)))
|
|
{
|
|
// If we didn't manage to work out a username, then just put the booking under the name of the current user.
|
|
// And if we haven't got a current user, then just use the email address.
|
|
// TODO: offer an option of choosing a default user?
|
|
$mrbs_user = session()->getCurrentUser();
|
|
$result = (isset($mrbs_user)) ? $mrbs_user->username : $email;
|
|
}
|
|
return $result;
|
|
break;
|
|
|
|
default:
|
|
throw new \InvalidArgumentException("Unknown value for import_creator: $import_creator");
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Given an RFC 5545 recurrence rule, returns a RepeatRule object giving the MRBS repeat
|
|
// details.
|
|
// Returns FALSE on failure with error messages being returned in the array $errors
|
|
function get_repeat_rule(string $rrule, int $start_time, array &$errors)
|
|
{
|
|
// Set up the result with safe defaults
|
|
$repeat_rule = new RepeatRule();
|
|
$repeat_rule->setType(RepeatRule::NONE);
|
|
$repeat_rule->setInterval(1);
|
|
$end_date = new DateTime();
|
|
$end_date->setTimestamp(0);
|
|
$repeat_rule->setEndDate($end_date);
|
|
|
|
$rules = array();
|
|
$recur_rule_parts = explode(';', $rrule);
|
|
foreach ($recur_rule_parts as $recur_rule_part)
|
|
{
|
|
list($name, $value) = explode('=', $recur_rule_part);
|
|
$rules[$name] = $value;
|
|
}
|
|
|
|
if (!isset($rules['FREQ']))
|
|
{
|
|
$errors[] = get_vocab("invalid_RRULE");
|
|
}
|
|
|
|
try
|
|
{
|
|
switch ($rules['FREQ'])
|
|
{
|
|
case 'DAILY':
|
|
$repeat_rule->setType(RepeatRule::DAILY);
|
|
break;
|
|
case 'WEEKLY':
|
|
$repeat_rule->setType(RepeatRule::WEEKLY);
|
|
if (isset($rules['BYDAY']))
|
|
{
|
|
$repeat_rule->setDaysFromRFC5545(explode(',', $rules['BYDAY']));
|
|
}
|
|
else
|
|
{
|
|
// If there's no repeat day specified in the RRULE then
|
|
// 'the day is gotten from "DTSTART"'
|
|
$repeat_rule->setDays(array(date('w', $start_time)));
|
|
}
|
|
break;
|
|
case 'MONTHLY':
|
|
$repeat_rule->setType(RepeatRule::MONTHLY);
|
|
if (!isset($rules['BYDAY']))
|
|
{
|
|
$repeat_rule->setMonthlyAbsolute((int)$rules['BYMONTHDAY']);
|
|
$repeat_rule->setMonthlyType(RepeatRule::MONTHLY_ABSOLUTE);
|
|
}
|
|
else
|
|
{
|
|
$byday_days = explode(',', $rules['BYDAY']);
|
|
if (count($byday_days) > 1)
|
|
{
|
|
$errors[] = get_vocab("more_than_one_BYDAY") . $rules['FREQ'];
|
|
}
|
|
foreach ($byday_days as $byday_day)
|
|
{
|
|
$rfc5545day = mb_substr($byday_day, -2); // the last two characters of the string
|
|
$nth = mb_substr($byday_day, 0, -2); // everything except the last two characters
|
|
if ($nth === '')
|
|
{
|
|
// "If an integer modifier is not present, it means all days of this
|
|
// type within the specified frequency. For example, within a MONTHLY
|
|
// rule, MO represents all Mondays within the month." [RFC 5545]
|
|
// So that comes to the same thing as a WEEKLY repeat
|
|
$repeat_rule->setType(RepeatRule::WEEKLY);
|
|
$repeat_rule->setDaysFromRFC5545(array($rfc5545day));
|
|
}
|
|
elseif (($nth == '5') || ($nth == '-5'))
|
|
{
|
|
$errors[] = get_vocab("BYDAY_equals_5") . " $nth$rfc5545day";
|
|
}
|
|
else
|
|
{
|
|
$repeat_rule->setMonthlyRelative($byday_day);
|
|
$repeat_rule->setMonthlyType(RepeatRule::MONTHLY_RELATIVE);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case 'YEARLY':
|
|
$repeat_rule->setType(RepeatRule::YEARLY);
|
|
break;
|
|
default:
|
|
$errors[] = get_vocab("unsupported_FREQ") . $rules['FREQ'];
|
|
break;
|
|
}
|
|
}
|
|
catch (RFC5545Exception $e)
|
|
{
|
|
$errors[] = $e->getMessage();
|
|
}
|
|
|
|
if (isset($rules['INTERVAL']) && ($rules['INTERVAL'] > 1))
|
|
{
|
|
$repeat_rule->setInterval((int) $rules['INTERVAL']);
|
|
}
|
|
else
|
|
{
|
|
$repeat_rule->setInterval(1);
|
|
}
|
|
|
|
if (isset($rules['UNTIL']))
|
|
{
|
|
// Strictly speaking "the value of the UNTIL rule part MUST have the same
|
|
// value type as the "DTSTART" property". So we should really tell getTimestamp()
|
|
// the value type. But "if the "DTSTART" property is specified as a date with UTC
|
|
// time or a date with local time and time zone reference, then the UNTIL rule
|
|
// part MUST be specified as a date with UTC time" - so in nearly all cases
|
|
// supported by MRBS the value will be a UTC time.
|
|
$repeat_end_date = new DateTime();
|
|
$repeat_end_date->setTimestamp(Property::convertDatetimeValue($rules['UNTIL']));
|
|
$repeat_rule->setEndDate($repeat_end_date);
|
|
}
|
|
elseif (isset($rules['COUNT']))
|
|
{
|
|
// It would be quite easy to support COUNT, but we haven't done so yet
|
|
$errors[] = get_vocab("unsupported_COUNT");
|
|
}
|
|
else
|
|
{
|
|
$errors[] = get_vocab("no_indefinite_repeats");
|
|
}
|
|
|
|
return (empty($errors)) ? $repeat_rule : false;
|
|
}
|
|
|
|
|
|
// Gets the id of the area/room with the LOCATION property value of $location,
|
|
// creating an area and room if allowed.
|
|
// Returns FALSE if it can't find an id or create an id, with an error message in $error
|
|
function get_room_id($location, &$error)
|
|
{
|
|
global $area_room_order, $area_room_delimiter, $area_room_create;
|
|
|
|
// If there's no delimiter we assume we've just been given a room name (that will
|
|
// have to be unique). Otherwise we split the location into its area and room parts
|
|
if (mb_strpos($location, $area_room_delimiter) === false)
|
|
{
|
|
$location_area = '';
|
|
$location_room = $location;
|
|
}
|
|
elseif ($area_room_order == 'area_room')
|
|
{
|
|
list($location_area, $location_room) = explode($area_room_delimiter, $location, 2);
|
|
}
|
|
else
|
|
{
|
|
list($location_room, $location_area) = explode($area_room_delimiter, $location, 2);
|
|
}
|
|
$location_area = trim($location_area);
|
|
$location_room = trim($location_room);
|
|
|
|
// Now search the database for the room
|
|
|
|
// Case 1: we've just been given a room name, in which case we hope it happens
|
|
// to be unique, because if we find more than one we won't know which one is intended
|
|
// and if we don't find one at all we won't be able to create it because we won't
|
|
// know which area to put it in.
|
|
if ($location_area == '')
|
|
{
|
|
$sql = "SELECT COUNT(*)
|
|
FROM " . _tbl('room') . "
|
|
WHERE room_name=?";
|
|
$count = db()->query1($sql, array($location_room));
|
|
|
|
if ($count == 0)
|
|
{
|
|
$error = "'$location_room': " . get_vocab("room_does_not_exist_no_area");
|
|
return false;
|
|
}
|
|
elseif ($count > 1)
|
|
{
|
|
$error = "'$location_room': " . get_vocab("room_not_unique_no_area");
|
|
return false;
|
|
}
|
|
else // we've got a unique room name
|
|
{
|
|
$sql = "SELECT id
|
|
FROM " . _tbl('room') . "
|
|
WHERE room_name=?
|
|
LIMIT 1";
|
|
$id = db()->query1($sql, array($location_room));
|
|
return $id;
|
|
}
|
|
}
|
|
|
|
// Case 2: we've got an area and room name
|
|
else
|
|
{
|
|
// First of all get the area id
|
|
$sql = "SELECT id
|
|
FROM " . _tbl('area') . "
|
|
WHERE area_name=?
|
|
LIMIT 1";
|
|
$area_id = db()->query1($sql, array($location_area));
|
|
if ($area_id < 0)
|
|
{
|
|
// The area does not exist - create it if we are allowed to
|
|
if (!$area_room_create)
|
|
{
|
|
$error = get_vocab("area_does_not_exist") . " '$location_area'";
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
echo get_vocab("creating_new_area") . " '$location_area'<br>\n";
|
|
$error_add_area = '';
|
|
$area_id = mrbsAddArea($location_area, $error_add_area);
|
|
if ($area_id === false)
|
|
{
|
|
$error = get_vocab("could_not_create_area") . " '$location_area'";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Now we've got the area_id get the room_id
|
|
$sql = "SELECT id
|
|
FROM " . _tbl('room') . "
|
|
WHERE room_name=?
|
|
AND area_id=?
|
|
LIMIT 1";
|
|
$room_id = db()->query1($sql, array($location_room, $area_id));
|
|
if ($room_id < 0)
|
|
{
|
|
// The room does not exist - create it if we are allowed to
|
|
if (!$area_room_create)
|
|
{
|
|
$error = get_vocab("room_does_not_exist") . " '$location_room'";
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
echo get_vocab("creating_new_room") . " '$location_room'<br>\n";
|
|
$error_add_room = '';
|
|
$room_id = mrbsAddRoom($location_room, $area_id, $error_add_room);
|
|
if ($room_id === false)
|
|
{
|
|
$error = get_vocab("could_not_create_room") . " '$location_room'";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return $room_id;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a VEVENT to the MRBS database.
|
|
*
|
|
* Ignores any subcomponents (eg a VALARM inside a VEVENT) as MRBS does not
|
|
* yet handle things like reminders.
|
|
*
|
|
* @return boolean TRUE on success, FALSE if the event wasn't added.
|
|
*/
|
|
function process_event(Event $event) : bool
|
|
{
|
|
global $import_default_room, $import_creator, $import_default_type, $import_past, $skip;
|
|
global $morningstarts, $morningstarts_minutes, $resolution;
|
|
global $booking_types;
|
|
global $ignore_location, $add_location;
|
|
|
|
// We are going to cache the settings ($resolution etc.) for the rooms
|
|
// in order to avoid lots of database lookups
|
|
static $room_settings = array();
|
|
|
|
$registration_keys = array(
|
|
'allow_registration',
|
|
'registrant_limit',
|
|
'registrant_limit_enabled',
|
|
'registration_opens',
|
|
'registration_opens_enabled',
|
|
'registration_closes',
|
|
'registration_closes_enabled'
|
|
);
|
|
|
|
// Set up the booking with some defaults
|
|
$registrants = array();
|
|
$booking = array();
|
|
$booking['awaiting_approval'] = false;
|
|
$booking['private'] = false;
|
|
$booking['tentative'] = false;
|
|
$repeat_rule = new RepeatRule();
|
|
$repeat_rule->setType(RepeatRule::NONE);
|
|
$booking['repeat_rule'] = $repeat_rule;
|
|
$booking['type'] = $import_default_type;
|
|
$booking['room_id'] = $import_default_room;
|
|
|
|
// Get the start time because we'll need it later.
|
|
$dt_start = $event->getProperties('DTSTART', 1)[0];
|
|
if (empty($dt_start))
|
|
{
|
|
trigger_error("No DTSTART", E_USER_WARNING);
|
|
}
|
|
$booking['start_time'] = $dt_start->toTimestamps()[0];
|
|
|
|
// Then iterate over the properties to get the rest of the details.
|
|
$problems = [];
|
|
$properties = $event->getProperties();
|
|
|
|
foreach ($properties as $property)
|
|
{
|
|
$name = $property->getName();
|
|
$values = $property->getValues();
|
|
|
|
switch ($name)
|
|
{
|
|
case 'ORGANIZER':
|
|
$booking['create_by'] = get_create_by($property, $import_creator);
|
|
$booking['modified_by'] = '';
|
|
break;
|
|
|
|
case 'SUMMARY':
|
|
$booking['name'] = $values[0];
|
|
break;
|
|
|
|
case 'DESCRIPTION':
|
|
$booking['description'] = $values[0];
|
|
break;
|
|
|
|
case 'LOCATION':
|
|
$location = $values[0]; // We may need the original LOCATION later
|
|
if ($ignore_location)
|
|
{
|
|
$booking['room_id'] = $import_default_room;
|
|
}
|
|
else
|
|
{
|
|
$error = '';
|
|
$booking['room_id'] = get_room_id($location, $error);
|
|
if ($booking['room_id'] === false)
|
|
{
|
|
$problems[] = $error;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'DTSTART':
|
|
// We've already handled this
|
|
break;
|
|
|
|
case 'DTEND':
|
|
$booking['end_time'] = $property->toTimestamps()[0];
|
|
break;
|
|
|
|
case 'DURATION':
|
|
trigger_error("DURATION not yet supported by MRBS", E_USER_WARNING);
|
|
break;
|
|
|
|
case 'RRULE':
|
|
$rrule_errors = [];
|
|
$repeat_rule = get_repeat_rule($values[0], $booking['start_time'], $rrule_errors);
|
|
if ($repeat_rule === false)
|
|
{
|
|
$problems = array_merge($problems, $rrule_errors);
|
|
}
|
|
else
|
|
{
|
|
$booking['repeat_rule'] = $repeat_rule;
|
|
}
|
|
break;
|
|
|
|
case 'EXDATE':
|
|
try
|
|
{
|
|
$booking['skip_list'] = $property->toTimestamps();
|
|
}
|
|
catch (\Exception $e)
|
|
{
|
|
// If it's a bad timezone, flag that as a problem, otherwise rethrow the exception
|
|
if (str_contains($e->getMessage(), 'Unknown or bad timezone'))
|
|
{
|
|
$problems[] = get_vocab('bad_timezone', $property->getParamValues('TZID')[0]);
|
|
}
|
|
else
|
|
{
|
|
throw $e;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'CLASS':
|
|
$booking['private'] = in_array($values[0], ['PRIVATE', 'CONFIDENTIAL']);
|
|
break;
|
|
|
|
case 'STATUS':
|
|
$booking['tentative'] = ($values[0] == 'TENTATIVE');
|
|
break;
|
|
|
|
case 'UID':
|
|
$booking['ical_uid'] = $values[0];
|
|
break;
|
|
|
|
case 'SEQUENCE':
|
|
$booking['ical_sequence'] = $values[0];
|
|
break;
|
|
|
|
case 'LAST-MODIFIED':
|
|
// TODO: We probably ought to do something with LAST-MODIFIED and use it for the timestamp field
|
|
break;
|
|
|
|
default:
|
|
// MRBS specific properties
|
|
$mrbs_prefix = 'X-MRBS-';
|
|
if (str_starts_with($name, $mrbs_prefix))
|
|
{
|
|
$key = substr($name, strlen($mrbs_prefix));
|
|
$key = strtolower($key);
|
|
// Convert hyphens back to underscores
|
|
$key = str_replace('-', '_', $key);
|
|
// Periods
|
|
if ($key == 'periods')
|
|
{
|
|
// The VEVENT was created by exporting from an MRBS area that uses periods and the periods
|
|
// were converted into real times. We can't import this back into MRBS, so we'll ignore it.
|
|
// TODO: Do something better. One option would be to use the Periods data that is in this
|
|
// TODO: property to convert the times back into periods. Another option would be to allow
|
|
// TODO: it to be imported into a Times mode area.
|
|
$problems[] = get_vocab('event_created_from_periods');
|
|
}
|
|
// Type
|
|
elseif ($key == 'type')
|
|
{
|
|
if (!empty($booking_types))
|
|
{
|
|
foreach ($booking_types as $type)
|
|
{
|
|
if ($values[0] == get_type_vocab($type))
|
|
{
|
|
$booking['type'] = $type;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Registration keys
|
|
elseif (in_array($key, $registration_keys))
|
|
{
|
|
$booking[$key] = $values[0];
|
|
}
|
|
// Registrants
|
|
elseif ($key == 'registrant')
|
|
{
|
|
$registrants[] = array(
|
|
'username' => $values[0],
|
|
'registered' => $property->getParamValues('X-MRBS-REGISTERED')[0],
|
|
'create_by' => $property->getParamValues('X-MRBS-CREATE-BY')[0]
|
|
);
|
|
}
|
|
// Custom fields
|
|
else
|
|
{
|
|
$booking[$key] = $values[0];
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$import_past && ($booking['end_time'] < time()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// A UID is mandatory in RFC 5545. We'll be lenient and provide one if it is missing
|
|
if (!isset($booking['ical_uid']))
|
|
{
|
|
$booking['ical_uid'] = generate_global_uid($booking['name']);
|
|
$booking['sequence'] = 0; // and we'll start the sequence from 0
|
|
}
|
|
|
|
// Modify the brief and/or full descriptions
|
|
if (!empty($add_location) && isset($location) && ($location !== ''))
|
|
{
|
|
// Brief description (SUMMARY)
|
|
if (in_array('summary', $add_location))
|
|
{
|
|
if (isset($booking['name']) && ($booking['name'] !== ''))
|
|
{
|
|
$booking['name'] = get_vocab('expanded_name',
|
|
$booking['name'],
|
|
$location);
|
|
}
|
|
else
|
|
{
|
|
$booking['name'] = get_vocab('expanded_empty_name', $location);
|
|
}
|
|
}
|
|
// Full description (DESCRIPTION)
|
|
if (in_array('description', $add_location))
|
|
{
|
|
if (isset($booking['description']) && ($booking['description'] !== ''))
|
|
{
|
|
$booking['description'] = get_vocab('expanded_description',
|
|
$booking['description'],
|
|
$location);
|
|
}
|
|
else
|
|
{
|
|
$booking['description'] = get_vocab('expanded_empty_description', $location);
|
|
}
|
|
}
|
|
}
|
|
|
|
// A SUMMARY is optional in RFC 5545, but a brief description is mandatory in MRBS.
|
|
// So if the VEVENT didn't include a name, we'll give it one.
|
|
if (!isset($booking['name']) || ($booking['name']) === '')
|
|
{
|
|
$tag = 'import_no_SUMMARY';
|
|
$booking['name'] = get_vocab($tag);
|
|
// Throw an exception if it is still empty - probably because the vocab string has
|
|
// been overridden in the config file by an empty string.
|
|
if (!isset($booking['name']) || ($booking['name']) === '')
|
|
{
|
|
throw new Exception("Vocab string for '$tag' is empty");
|
|
}
|
|
}
|
|
|
|
// LOCATION is optional in RFC 5545 but is obviously mandatory in MRBS.
|
|
// If there is no LOCATION property, we use the default_room specified on
|
|
// the form, but if there is no default room (most likely because no rooms
|
|
// have been created), then this error message is created.
|
|
if (!isset($booking['room_id']))
|
|
{
|
|
$problems[] = get_vocab("no_LOCATION");
|
|
}
|
|
|
|
if (empty($problems))
|
|
{
|
|
// Get the area settings for this room if we haven't got them already
|
|
if (!isset($room_settings[$booking['room_id']]))
|
|
{
|
|
get_area_settings(get_area($booking['room_id']));
|
|
$room_settings[$booking['room_id']]['morningstarts'] = $morningstarts;
|
|
$room_settings[$booking['room_id']]['morningstarts_minutes'] = $morningstarts_minutes;
|
|
$room_settings[$booking['room_id']]['resolution'] = $resolution;
|
|
}
|
|
// Round the start and end times to slot boundaries
|
|
$date = getdate($booking['start_time']);
|
|
$m = $date['mon'];
|
|
$d = $date['mday'];
|
|
$y = $date['year'];
|
|
$am7 = mktime($room_settings[$booking['room_id']]['morningstarts'],
|
|
$room_settings[$booking['room_id']]['morningstarts_minutes'],
|
|
0, $m, $d, $y);
|
|
$booking['start_time'] = round_t_down($booking['start_time'],
|
|
$room_settings[$booking['room_id']]['resolution'],
|
|
$am7);
|
|
$booking['end_time'] = round_t_up($booking['end_time'],
|
|
$room_settings[$booking['room_id']]['resolution'],
|
|
$am7);
|
|
// Make the bookings
|
|
$bookings = array($booking);
|
|
$result = mrbsMakeBookings($bookings, null, false, $skip);
|
|
|
|
if ($result['valid_booking'])
|
|
{
|
|
// If the bookings have been made, then add the registrants
|
|
add_registrants($result['new_details'][0]['id'], $registrants);
|
|
return true;
|
|
}
|
|
}
|
|
// There were problems - list them
|
|
echo "<div class=\"problem_report\">\n";
|
|
echo escape_html(get_vocab("could_not_import", $booking['name'], $booking['ical_uid']));
|
|
echo "<ul>\n";
|
|
foreach ($problems as $problem)
|
|
{
|
|
echo "<li>" . escape_html($problem) . "</li>\n";
|
|
}
|
|
if (!empty($result['violations']['errors']))
|
|
{
|
|
echo "<li>" . get_vocab("rules_broken") . "\n";
|
|
echo "<ul>\n";
|
|
foreach ($result['violations']['errors'] as $rule)
|
|
{
|
|
echo "<li>$rule</li>\n";
|
|
}
|
|
echo "</ul></li>\n";
|
|
}
|
|
if (!empty($result['conflicts']))
|
|
{
|
|
echo "<li>" . get_vocab("conflict"). "\n";
|
|
echo "<ul>\n";
|
|
foreach ($result['conflicts'] as $conflict)
|
|
{
|
|
echo "<li>$conflict</li>\n";
|
|
}
|
|
echo "</ul></li>\n";
|
|
}
|
|
echo "</ul>\n";
|
|
echo "</div>\n";
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
function get_file_details_url(string $file) : array
|
|
{
|
|
$files = array();
|
|
list( , $tmp_name) = explode('://', $file, 2);
|
|
$files[] = array('name' => $file,
|
|
'tmp_name' => $tmp_name,
|
|
'size' => null);
|
|
return $files;
|
|
}
|
|
|
|
|
|
function get_file_details_calendar(array $file) : array
|
|
{
|
|
$files = array();
|
|
$files[] = array('name' => $file['name'],
|
|
'tmp_name' => $file['tmp_name'],
|
|
'size' => filesize($file['tmp_name']));
|
|
return $files;
|
|
}
|
|
|
|
|
|
function get_file_details_bzip2(array $file) : array
|
|
{
|
|
// It's not possible to get the uncompressed size of a bzip2 file without first
|
|
// decompressing the whole file
|
|
$files = array();
|
|
$files[] = array('name' => $file['name'],
|
|
'tmp_name' => $file['tmp_name'],
|
|
'size' => null);
|
|
return $files;
|
|
}
|
|
|
|
function get_file_details_gzip(array $file) : array
|
|
{
|
|
// Get the uncompressed size of the gzip file which is stored in the last four
|
|
// bytes of the file, little-endian
|
|
if (false !== ($handle = fopen($file['tmp_name'], 'rb')))
|
|
{
|
|
fseek($handle, -4, SEEK_END);
|
|
$buffer = fread($handle, 4);
|
|
$size_array = unpack('V', $buffer);
|
|
$size = end($size_array);
|
|
fclose($handle);
|
|
}
|
|
else
|
|
{
|
|
$size = null;
|
|
}
|
|
$files = array();
|
|
$files[] = array('name' => $file['name'],
|
|
'tmp_name' => $file['tmp_name'],
|
|
'size' => $size);
|
|
return $files;
|
|
}
|
|
|
|
|
|
function get_file_details_zip(array $file) : array
|
|
{
|
|
$files = array();
|
|
|
|
if (class_exists('ZipArchive'))
|
|
{
|
|
$zip = new ZipArchive();
|
|
|
|
if (true === ($result = $zip->open($file['tmp_name'])))
|
|
{
|
|
for ($i=0; $i<$zip->numFiles; $i++)
|
|
{
|
|
$details = array();
|
|
$stats = $zip->statIndex($i);
|
|
$details['name'] = $stats['name'];
|
|
$details['tmp_name'] = $file['tmp_name'] . '#' . $stats['name'];
|
|
$details['size'] = $stats['size'];
|
|
$files[] = $details;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Try and convert the error code into something a bit more
|
|
// meaningful.
|
|
//
|
|
// It's safe to use ReflectionClass (PHP 5) as we already know that
|
|
// ZipArchive (PHP 5.2.0) exists
|
|
$reflection = new ReflectionClass('ZipArchive');
|
|
$constants = $reflection->getConstants();
|
|
foreach ($constants as $key => $value)
|
|
{
|
|
if (($result === $value) && (mb_strpos($key, 'ER_') === 0))
|
|
{
|
|
$error_code = $key;
|
|
break;
|
|
}
|
|
}
|
|
$message = "ZipArchive::open() failed with ";
|
|
if (isset($error_code))
|
|
{
|
|
$message .= "error code $error_code.";
|
|
// There is a problem on IIS when opening a ZipArchive file that is in
|
|
// C:\Windows\Temp. ER_OPEN will be returned unless the user IUSR_XXXX
|
|
// has 'List Folder' permission.
|
|
// See the user notes at http://php.net/manual/en/ziparchive.open.php
|
|
// and also https://bugs.php.net/bug.php?id=54128
|
|
if ($result == ZipArchive::ER_OPEN)
|
|
{
|
|
$message .= " If your server is running Windows, check the permissions on " .
|
|
dirname($file['tmp_name']) . ". 'List Folder' permission is required " .
|
|
"for user IUSR_XXXX.";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$message .= "unknown error code '$result'";
|
|
}
|
|
trigger_error($message, E_USER_WARNING);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
trigger_error("Could not open zip archive - the ZipArchive class does not exist on this system", E_USER_WARNING);
|
|
}
|
|
return $files;
|
|
}
|
|
|
|
|
|
function get_details($file) : array
|
|
{
|
|
$result = array();
|
|
|
|
if (is_string($file))
|
|
{
|
|
list($result['wrapper']) = explode('://', $file, 2);
|
|
$result['files'] = get_file_details_url($file);
|
|
}
|
|
else
|
|
{
|
|
switch ($file['type'])
|
|
{
|
|
case 'text/calendar':
|
|
case 'text/html':
|
|
case 'text/plain':
|
|
case 'application/x-download':
|
|
$result['wrapper'] = 'file';
|
|
$result['files'] = get_file_details_calendar($file);
|
|
break;
|
|
case 'application/x-bzip2':
|
|
$result['wrapper'] = 'compress.bzip2';
|
|
$result['files'] = get_file_details_bzip2($file);
|
|
break;
|
|
case 'application/x-gzip':
|
|
$result['wrapper'] = 'compress.zlib';
|
|
$result['files'] = get_file_details_gzip($file);
|
|
break;
|
|
case 'application/x-zip-compressed':
|
|
case 'application/zip':
|
|
$result['wrapper'] = 'zip';
|
|
$result['files'] = get_file_details_zip($file);
|
|
break;
|
|
default:
|
|
$result = false;
|
|
trigger_error("Unknown file type '" . $file['type'] . "'", E_USER_NOTICE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
function get_fieldset_source(array $compression_wrappers) : ElementFieldset
|
|
{
|
|
global $wrapper_mime_types;
|
|
global $source_type, $url;
|
|
|
|
$fieldset = new ElementFieldset();
|
|
|
|
$fieldset->addLegend(get_vocab('source'));
|
|
|
|
// Source type
|
|
$field = new FieldInputRadioGroup();
|
|
$options = array('file' => get_vocab('file'),
|
|
'url' => get_vocab('url'));
|
|
$field->setLabel(get_vocab('source_type'))
|
|
->addRadioOptions($options, 'source_type', $source_type, true);
|
|
$fieldset->addElement($field);
|
|
|
|
// File
|
|
$field = new FieldInputFile();
|
|
|
|
$accept_mime_types = array();
|
|
foreach ($compression_wrappers as $compression_wrapper)
|
|
{
|
|
$accept_mime_types[] = $wrapper_mime_types[$compression_wrapper];
|
|
}
|
|
// 'file' will always be available. Put it at the beginning of the array.
|
|
array_unshift($accept_mime_types, $wrapper_mime_types['file']);
|
|
|
|
$field->setLabel(get_vocab('file_name'))
|
|
->setAttribute('id', 'field_file')
|
|
->setControlAttributes(array(
|
|
'accept' => implode(',', $accept_mime_types),
|
|
'name' => 'upload_file',
|
|
'id' => 'upload_file')
|
|
);
|
|
|
|
$fieldset->addElement($field);
|
|
|
|
// URL
|
|
$field = new FieldInputUrl();
|
|
$field->setLabel(get_vocab('url'))
|
|
->setAttribute('id', 'field_url')
|
|
->setControlAttributes(array(
|
|
'name' => 'url',
|
|
'id' => 'url',
|
|
'required' => true,
|
|
'value' => $url)
|
|
);
|
|
$fieldset->addElement($field);
|
|
|
|
return $fieldset;
|
|
}
|
|
|
|
|
|
function get_fieldset_location_parsing() : ElementFieldset
|
|
{
|
|
global $area_room_order, $area_room_delimiter, $area_room_create;
|
|
|
|
$fieldset = new ElementFieldset();
|
|
$fieldset->setAttribute('id', 'location_parsing');
|
|
|
|
// Area-room order
|
|
$field = new FieldInputRadioGroup();
|
|
$options = array('area_room' => get_vocab('area_room'),
|
|
'room_area' => get_vocab('room_area'));
|
|
$field->setLabel(get_vocab('area_room_order'))
|
|
->setLabelAttribute('title', get_vocab('area_room_order_note'))
|
|
->addRadioOptions($options, 'area_room_order', $area_room_order, true);
|
|
$fieldset->addElement($field);
|
|
|
|
// Area-room delimiter
|
|
$field = new FieldInputText();
|
|
$field->setLabel(get_vocab('area_room_delimiter'))
|
|
->setLabelAttribute('title', get_vocab('area_room_delimiter_note'))
|
|
->setControlAttributes(array('name' => 'area_room_delimiter',
|
|
'value' => $area_room_delimiter,
|
|
'class' => 'short',
|
|
'required' => true));
|
|
$fieldset->addElement($field);
|
|
|
|
// Area/room create
|
|
$field =new FieldInputCheckbox();
|
|
$field->setLabel(get_vocab('area_room_create'))
|
|
->setControlAttribute('name', 'area_room_create')
|
|
->setChecked($area_room_create);
|
|
$fieldset->addElement($field);
|
|
|
|
return $fieldset;
|
|
}
|
|
|
|
|
|
function get_fieldset_ignore_location_settings() : ElementFieldset
|
|
{
|
|
global $add_location;
|
|
|
|
$fieldset = new ElementFieldset();
|
|
$fieldset->setAttribute('id', 'ignore_location_settings');
|
|
|
|
// Add the location to the brief and/or full description
|
|
$field =new FieldInputCheckboxGroup();
|
|
$options = array(
|
|
'summary' => get_vocab('namebooker'),
|
|
'description' => get_vocab('fulldescription_short')
|
|
);
|
|
$field->setLabel(get_vocab('add_location'))
|
|
->setControlAttribute('name', 'add_location')
|
|
->addCheckboxOptions($options, 'add_location[]', $add_location);
|
|
$fieldset->addElement($field);
|
|
|
|
return $fieldset;
|
|
}
|
|
|
|
|
|
function get_fieldset_location_settings() : ElementFieldset
|
|
{
|
|
global $import_default_room;
|
|
global $ignore_location;
|
|
|
|
$fieldset = new ElementFieldset();
|
|
|
|
$fieldset->addLegend(get_vocab('area_room_settings'));
|
|
|
|
// Default room
|
|
$areas = get_area_names(true);
|
|
if (count($areas) > 0)
|
|
{
|
|
$options = array();
|
|
|
|
foreach($areas as $area_id => $area_name)
|
|
{
|
|
$rooms = get_room_names($area_id, $all=true);
|
|
if (count($rooms) > 0)
|
|
{
|
|
$options[$area_name] = array();
|
|
foreach($rooms as $room_id => $room_name)
|
|
{
|
|
$options[$area_name][$room_id] = $room_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (count($options) > 0)
|
|
{
|
|
$field = new FieldSelect();
|
|
|
|
$field->setLabel(get_vocab('default_room'))
|
|
->setLabelAttribute('title', get_vocab('default_room_note'))
|
|
->setControlAttribute('name', 'import_default_room')
|
|
->addSelectOptions($options, $import_default_room, true);
|
|
|
|
$fieldset->addElement($field);
|
|
}
|
|
}
|
|
|
|
// Ignore location
|
|
$field =new FieldInputCheckbox();
|
|
$field->setLabel(get_vocab('ignore_location'))
|
|
->setControlAttribute('name', 'ignore_location')
|
|
->setChecked($ignore_location);
|
|
$fieldset->addElement($field);
|
|
|
|
// Location parsing fieldset
|
|
$fieldset->addElement(get_fieldset_location_parsing());
|
|
|
|
// Settings when we are ignoring the location
|
|
$fieldset->addElement(get_fieldset_ignore_location_settings());
|
|
|
|
return $fieldset;
|
|
}
|
|
|
|
|
|
function get_fieldset_other_settings() : ElementFieldset
|
|
{
|
|
// TODO: If the auth type is 'db', then we could offer to create users if necessary. Maybe only if the
|
|
// TODO: X-MRBS-USERNAME parameter is present? And should the user details be updated if the display name
|
|
// TODO: and email address for an existing username have changed? Or maybe just issue a list of differences?
|
|
|
|
global $import_default_type, $import_past, $skip;
|
|
|
|
$fieldset = new ElementFieldset();
|
|
|
|
$fieldset->addLegend(get_vocab('other_settings'));
|
|
|
|
// Creator
|
|
// The default is to derive the creator from the organizer's MRBS username, because if that isn't present it
|
|
// will be derived automatically from the organizer's email address. It's useful to have the option of using
|
|
// the email address because there may be occasions when the iCalendar file is being imported into a system that
|
|
// has a different set of usernames to the one that it was exported from.
|
|
$options = [
|
|
IMPORT_CREATOR_USERNAME => get_vocab('organizer_mrbs_username'),
|
|
IMPORT_CREATOR_EMAIL => get_vocab('organizer_email_address')
|
|
];
|
|
$value = IMPORT_CREATOR_USERNAME;
|
|
$field = new FieldInputRadioGroup();
|
|
$field->setControlAttribute('id', 'import_creator')
|
|
->setLabel(get_vocab('derive_creator_from'))
|
|
->addRadioOptions($options, 'import_creator', $value, true);
|
|
$fieldset->addElement($field);
|
|
|
|
// Default type
|
|
$field = new FieldSelect();
|
|
$options = get_type_options(true);
|
|
if (count($options) > 1)
|
|
{
|
|
$field->setLabel(get_vocab('default_type'))
|
|
->setControlAttribute('name', 'import_default_type')
|
|
->addSelectOptions($options, $import_default_type, true);
|
|
$fieldset->addElement($field);
|
|
}
|
|
|
|
// Import past bookings
|
|
// Add a hidden element so that if the checkbox is not checked we
|
|
// get 0 instead of NULL passed to the server and so the default
|
|
// can be used.
|
|
// TODO: need a better way of doing this
|
|
$hidden = new ElementInputHidden();
|
|
$hidden->setAttributes(array(
|
|
'name' => 'import_past',
|
|
'value' => 0
|
|
));
|
|
$fieldset->addElement($hidden);
|
|
$field = new FieldInputCheckbox();
|
|
$field->setLabel(get_vocab('import_past'))
|
|
->setControlAttribute('name', 'import_past')
|
|
->setChecked($import_past);
|
|
$fieldset->addElement($field);
|
|
|
|
// Skip conflicts
|
|
// Add a hidden element (see comment above)
|
|
$hidden = new ElementInputHidden();
|
|
$hidden->setAttributes(array(
|
|
'name' => 'skip',
|
|
'value' => 0
|
|
));
|
|
$fieldset->addElement($hidden);
|
|
$field = new FieldInputCheckbox();
|
|
$field->setLabel(get_vocab('skip_conflicts'))
|
|
->setControlAttribute('name', 'skip')
|
|
->setChecked($skip);
|
|
$fieldset->addElement($field);
|
|
|
|
return $fieldset;
|
|
}
|
|
|
|
|
|
function get_fieldset_submit_button() : ElementFieldset
|
|
{
|
|
$fieldset = new ElementFieldset();
|
|
|
|
// The Submit button
|
|
$field = new FieldInputSubmit();
|
|
$field->setControlAttributes(array('name' => 'import',
|
|
'value' => get_vocab('import')));
|
|
$fieldset->addElement($field);
|
|
|
|
return $fieldset;
|
|
}
|
|
|
|
|
|
$import = get_form_var('import', 'string');
|
|
$source_type = get_form_var('source_type', 'string', $default_import_source);
|
|
$url = get_form_var('url', 'string');
|
|
$import_default_room = get_form_var('import_default_room', 'int');
|
|
$ignore_location = get_form_var('ignore_location', 'string', '0');
|
|
$add_location = get_form_var('add_location', 'array');
|
|
$area_room_order = get_form_var('area_room_order', 'string', 'area_room');
|
|
$area_room_delimiter = get_form_var('area_room_delimiter', 'string', $default_area_room_delimiter);
|
|
$area_room_create = get_form_var('area_room_create', 'string', '0');
|
|
$import_creator = get_form_var('import_creator', 'string', IMPORT_CREATOR_USERNAME);
|
|
$import_default_type = get_form_var('import_default_type', 'string', $default_type);
|
|
$import_past = get_form_var('import_past', 'string', ((empty($default_import_past)) ? '0' : '1'));
|
|
$skip = get_form_var('skip', 'bool', empty($skip_default));
|
|
|
|
// Check the CSRF token if we're being asked to import data
|
|
if (!empty($import))
|
|
{
|
|
Form::checkToken();
|
|
}
|
|
|
|
// Check the user is authorised for this page
|
|
checkAuthorised(this_page());
|
|
|
|
$context = array(
|
|
'view' => $view,
|
|
'view_all' => $view_all,
|
|
'year' => $year,
|
|
'month' => $month,
|
|
'day' => $day,
|
|
'area' => $area,
|
|
'room' => $room ?? null
|
|
);
|
|
|
|
print_header($context);
|
|
|
|
|
|
// PHASE 2 - Process the files
|
|
// ---------------------------
|
|
|
|
if (!empty($import))
|
|
{
|
|
if ($source_type == 'url')
|
|
{
|
|
if (!isset($url) || (false === (url_validate_not_local($url))))
|
|
{
|
|
echo "<p>\n";
|
|
echo get_vocab("invalid_url");
|
|
echo "</p>\n";
|
|
}
|
|
else
|
|
{
|
|
$details = get_details($url);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (($_FILES['upload_file']['error'] !== UPLOAD_ERR_OK) ||
|
|
!is_uploaded_file($_FILES['upload_file']['tmp_name']))
|
|
{
|
|
echo "<p>\n";
|
|
echo get_vocab("upload_failed");
|
|
|
|
if ($_FILES['upload_file']['error'] !== UPLOAD_ERR_OK)
|
|
{
|
|
try
|
|
{
|
|
throw new UploadException($_FILES['upload_file']['error']);
|
|
}
|
|
catch (UploadException $e)
|
|
{
|
|
switch ($e->getCode())
|
|
{
|
|
case UPLOAD_ERR_INI_SIZE:
|
|
case UPLOAD_ERR_FORM_SIZE:
|
|
echo "<br>\n" . get_vocab("max_allowed_file_size", ini_get('upload_max_filesize'));
|
|
break;
|
|
case UPLOAD_ERR_NO_FILE:
|
|
echo "<br>\n" . get_vocab("no_file");
|
|
break;
|
|
default:
|
|
// None of the other possible errors would make much sense to the user, but should be reported
|
|
trigger_error($e->getMessage(), E_USER_WARNING);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Check this last, as it will be true if there is an error
|
|
elseif (!is_uploaded_file($_FILES['upload_file']['tmp_name']))
|
|
{
|
|
// This should not happen and if it does may mean that somebody is messing about
|
|
trigger_error("Attempt to import a file that has not been uploaded", E_USER_WARNING);
|
|
}
|
|
|
|
echo "</p>\n";
|
|
}
|
|
else
|
|
{
|
|
// We've got a file
|
|
$details = get_details($_FILES['upload_file']);
|
|
}
|
|
}
|
|
|
|
if (isset($details))
|
|
{
|
|
if ($details === false)
|
|
{
|
|
echo "<p>" . get_vocab("could_not_process") . "</p>\n";
|
|
}
|
|
else
|
|
{
|
|
foreach ($details['files'] as $file)
|
|
{
|
|
echo "<h3>" . $file['name'] . "</h3>";
|
|
|
|
$n_success = 0;
|
|
$n_failure = 0;
|
|
|
|
$handle = fopen($details['wrapper'] . '://' . $file['tmp_name'], 'rb');
|
|
|
|
if ($handle === false)
|
|
{
|
|
echo "<p>" . get_vocab("could_not_process") . "</p>\n";
|
|
}
|
|
else
|
|
{
|
|
while (false !== ($vevent = ComponentFactory::getNextFromStream($handle, Event::NAME)))
|
|
{
|
|
(process_event($vevent)) ? $n_success++ : $n_failure++;
|
|
}
|
|
|
|
fclose($handle);
|
|
|
|
echo "<p>\n";
|
|
echo "$n_success " . get_vocab("events_imported");
|
|
if ($n_failure > 0)
|
|
{
|
|
echo "<br>\n$n_failure " . get_vocab("events_not_imported");
|
|
}
|
|
echo "</p>\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// PHASE 1 - Get the user input
|
|
// ----------------------------
|
|
|
|
echo "<h2>" . get_vocab('import_icalendar') . "</h2>\n";
|
|
|
|
$compression_wrappers = get_compression_wrappers();
|
|
|
|
echo "<p>\n" . get_vocab('import_intro') . "</p>\n";
|
|
echo "<p>\n" . get_vocab('supported_file_types') . "</p>\n";
|
|
echo "<ul>\n";
|
|
echo "<li>" . $wrapper_descriptions['file'] . "</li>\n";
|
|
foreach ($compression_wrappers as $compression_wrapper)
|
|
{
|
|
echo "<li>" . $wrapper_descriptions[$compression_wrapper] . "</li>\n";
|
|
}
|
|
echo "</ul>\n";
|
|
|
|
$form = new Form(Form::METHOD_POST);
|
|
|
|
$form->setAttributes(array('class' => 'standard',
|
|
'enctype' => 'multipart/form-data',
|
|
'action' => multisite(this_page())));
|
|
|
|
$fieldset = new ElementFieldset();
|
|
|
|
$fieldset->addElement(get_fieldset_source($compression_wrappers))
|
|
->addElement(get_fieldset_location_settings())
|
|
->addElement(get_fieldset_other_settings())
|
|
->addElement(get_fieldset_submit_button());
|
|
|
|
$form->addElement($fieldset);
|
|
|
|
$form->render();
|
|
|
|
|
|
print_footer();
|