'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'
\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'
\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 "
\n"; echo get_vocab("invalid_url"); echo "
\n"; } else { $details = get_details($url); } } else { if (($_FILES['upload_file']['error'] !== UPLOAD_ERR_OK) || !is_uploaded_file($_FILES['upload_file']['tmp_name'])) { echo "\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 "
\n" . get_vocab("max_allowed_file_size", ini_get('upload_max_filesize'));
break;
case UPLOAD_ERR_NO_FILE:
echo "
\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 "
" . get_vocab("could_not_process") . "
\n"; } else { foreach ($details['files'] as $file) { echo "" . get_vocab("could_not_process") . "
\n"; } else { while (false !== ($vevent = ComponentFactory::getNextFromStream($handle, Event::NAME))) { (process_event($vevent)) ? $n_success++ : $n_failure++; } fclose($handle); echo "\n";
echo "$n_success " . get_vocab("events_imported");
if ($n_failure > 0)
{
echo "
\n$n_failure " . get_vocab("events_not_imported");
}
echo "
\n" . get_vocab('import_intro') . "
\n"; echo "\n" . get_vocab('supported_file_types') . "
\n"; echo "