'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 escape_html(get_vocab("could_not_import", $booking['name'], $booking['ical_uid'])); echo "\n"; echo "
\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 "

\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 "

\n"; } else { // We've got a file $details = get_details($_FILES['upload_file']); } } if (isset($details)) { if ($details === false) { echo "

" . get_vocab("could_not_process") . "

\n"; } else { foreach ($details['files'] as $file) { echo "

" . $file['name'] . "

"; $n_success = 0; $n_failure = 0; $handle = fopen($details['wrapper'] . '://' . $file['tmp_name'], 'rb'); if ($handle === false) { 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"; } } } } } // PHASE 1 - Get the user input // ---------------------------- echo "

" . get_vocab('import_icalendar') . "

\n"; $compression_wrappers = get_compression_wrappers(); echo "

\n" . get_vocab('import_intro') . "

\n"; echo "

\n" . get_vocab('supported_file_types') . "

\n"; echo "\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();