Files
mrbs-equbao-2026/import.php
T
人事系统开发 1ba6efd8ed MRBS 1.12.2 等保2.0二级整改完整提交
包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、JS空集合保护、
会话过期体验优化(403 JSON)、display_errors 关闭、
固定 key 根治 Integrity check failed 等全部改动

注意:config.inc.php/.htaccess/.user.ini 含敏感信息,
通过 .gitignore 排除,勿推送到公开仓库。
2026-09-09 16:55:02 +08:00

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();