WIP: add first version of csv fetching endpoint for lvplans

This commit is contained in:
Ivymaster
2026-07-20 15:18:49 +02:00
parent 0a9b1c26d3
commit f7762523f3
4 changed files with 524 additions and 0 deletions
+1
View File
@@ -127,6 +127,7 @@ $route['lvplan/caldav'] = 'LvPlanCalDAV';
$route['lvplan/caldav/.*'] = 'LvPlanCalDAV';
$route['lvplan/ical/subscription'] = 'LvPlanICALSubscription';
$route['lvplan/ical/download'] = 'LvPlanICALDownload';
$route['lvplan/csv/download'] = 'LvPlanCSVDownload';
// load routes from extensions, also look for environment-specific configs
$subdirs = ['application/config/extensions', 'application/config/' . ENVIRONMENT . '/extensions'];
@@ -0,0 +1,177 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
require_once(FHCPATH.'include/functions.inc.php');
class LvPlanCSVDownload extends Auth_Controller
{
const MAX_DATE_RANGE = 34560000; // 400 days
/**
* Object initialization.
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
$this->load->model('person/Benutzer_model', 'BenutzerModel');
}
/**
* Serves an authenticated CSV file download.
*
* @return void
*/
public function index()
{
$type = $this->getType();
$uid = $this->getUid();
$target = $this->getTarget();
list($begin, $ende) = $this->getDateRange();
$this->authorizeUid($uid);
$this->validateUid($uid);
$this->load->library('LvPlanCSVLib');
$content = $this->lvplancsvlib->getContent(
$uid,
$begin,
$ende,
$type,
$target
);
$filename = 'FH-Kalender_'.date('m_Y', $begin);
if ($target === 'outlook')
$filename .= '_outlook';
$filename .= '.csv';
$this->output
->set_content_type('text/csv', 'UTF-8')
->set_header('Content-Disposition: attachment; filename="'.$filename.'"')
->set_header('Pragma: public')
->set_header('Expires: 0')
->set_output($content);
}
/**
* @return string Requested timetable type.
*/
private function getType()
{
$type = $this->input->get('type', true);
if (!in_array($type, array('student', 'lektor'), true))
show_error('Ungueltiger Parameter: type', 400);
return $type;
}
/**
* @return string Requested user UID.
*/
private function getUid()
{
$uid = $this->input->get('pers_uid', true);
if (!is_string($uid) || $uid === '' || !check_utf8($uid))
show_error('Ungueltiger Parameter: pers_uid', 400);
return $uid;
}
/**
* @return string|null Requested CSV target.
*/
private function getTarget()
{
$target = $this->input->get('target', true);
if ($target !== null && $target !== 'outlook')
show_error('Ungueltiger Parameter: target', 400);
return $target;
}
/**
* Validates and expands the requested range to complete weeks.
*
* @return array{0: int, 1: int} Begin and end timestamps.
*/
private function getDateRange()
{
$begin = $this->getTimestamp('begin');
$ende = $this->getTimestamp('ende');
if ($ende < $begin)
show_error('Ungueltiger Datumsbereich', 400);
if ($ende - $begin > self::MAX_DATE_RANGE)
show_error('Datumsbereich zu gross', 400);
$begin = strtotime('monday this week', $begin);
$ende = strtotime('sunday this week', $ende);
return array($begin, $ende);
}
/**
* @param string $name GET parameter name.
* @return int Unix timestamp.
*/
private function getTimestamp($name)
{
$value = $this->input->get($name, true);
if (!is_string($value) || !ctype_digit($value))
show_error('Ungueltiger Parameter: '.$name, 400);
$timestamp = filter_var(
$value,
FILTER_VALIDATE_INT,
array('options' => array('min_range' => 0))
);
if ($timestamp === false)
show_error('Ungueltiger Parameter: '.$name, 400);
return $timestamp;
}
/**
* Ensures access to another user's timetable is explicitly permitted.
*
* @param string $uid Requested user UID.
* @return void
*/
private function authorizeUid($uid)
{
if ($uid !== getAuthUID()
&& !$this->permissionlib->isBerechtigt('basis/other_lv_plan'))
{
show_error('Forbidden', 403);
}
}
/**
* Ensures the requested user exists.
*
* @param string $uid Requested user UID.
* @return void
*/
private function validateUid($uid)
{
$this->BenutzerModel->addSelect('uid');
$user = $this->BenutzerModel->load(array($uid));
if (isError($user))
show_error(getError($user), 500);
if (!hasData($user))
show_error('Ungueltiger Parameter: pers_uid', 400);
}
}
+338
View File
@@ -0,0 +1,338 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class LvPlanCSVLib
{
const TARGET_OUTLOOK = 'outlook';
const GENERIC_HEADER = array(
'title',
'category',
'location',
'description',
'keywords',
'start_date',
'start_time',
'end_date',
'end_time',
'alarm',
'recur_type',
'recur_end_date',
'recur_interval',
'recur_data'
);
const OUTLOOK_HEADER = array(
'Betreff',
'Beginnt am',
'Beginnt um',
'Endet am',
'Endet um',
'Ganztaegiges Ereignis',
'Erinnerung Ein/Aus',
'Erinnerung am',
'Erinnerung um',
'Besprechungsplanung',
'Erforderliche Teilnehmer',
'Optionale Teilnehmer',
'Besprechungsressourcen',
'Abrechnungsinformationen',
'Beschreibung',
'Kategorien',
'Ort',
'Priorität',
'Privat',
'Reisekilometer',
'Vertraulichkeit',
'Zeitspanne zeigen als'
);
private $ci;
public function __construct()
{
$this->ci =& get_instance();
}
/**
* Builds CSV content for a user's timetable.
*
* @param string $uid User UID.
* @param int $begin First included date as Unix timestamp.
* @param int $ende Last included date as Unix timestamp.
* @param string $type Timetable type (student or lektor).
* @param string|null $target Optional CSV target (outlook).
* @return string
*/
public function getContent($uid, $begin, $ende, $type, $target = null)
{
if (!in_array($type, array('student', 'lektor'), true))
throw new InvalidArgumentException('Unsupported timetable type');
if ($target !== null && $target !== self::TARGET_OUTLOOK)
throw new InvalidArgumentException('Unsupported CSV target');
$isLecturer = $type === 'lektor';
$this->ci->load->library('KalenderLib', array('uid' => $uid));
$startDate = date('Y-m-d', $begin);
$endDate = date('Y-m-d', $ende);
if ($isLecturer)
{
$events = $this->ci->kalenderlib
->getPlanForLecturerByLecturer($startDate, $endDate, $uid);
}
else
{
$events = $this->ci->kalenderlib
->getPlanForStudentByStudent($startDate, $endDate, $uid);
}
if (!is_array($events))
$events = array();
$content = $this->buildCSVLine(
$target === self::TARGET_OUTLOOK ? self::OUTLOOK_HEADER : self::GENERIC_HEADER
);
foreach ($events as $event)
{
$eventData = $this->getEventData($event);
if ($eventData === null)
continue;
$content .= $this->buildCSVLine(
$target === self::TARGET_OUTLOOK
? $this->buildOutlookRow($eventData)
: $this->buildGenericRow($eventData)
);
}
return $content;
}
/**
* @param object $event Calendar event.
* @return array|null Normalized CSV values or null for invalid dates.
*/
private function getEventData($event)
{
$start = $this->createDateTime(isset($event->isostart) ? $event->isostart : null);
$end = $this->createDateTime(isset($event->isoend) ? $event->isoend : null);
if ($start === null || $end === null)
return null;
$timezone = new DateTimeZone('Europe/Vienna');
$start->setTimezone($timezone);
$end->setTimezone($timezone);
$isReservation = isset($event->type) && $event->type === 'reservierung';
$title = $isReservation
? (isset($event->titel) ? $event->titel : '')
: $this->joinValues(isset($event->topic) ? $event->topic : array());
$location = $this->joinValues(
isset($event->ort_kurzbz) ? $event->ort_kurzbz : array(),
' / '
);
$groups = $this->getGroups($event);
$lecturers = $this->getLecturers($event);
if ($isReservation)
{
$description = isset($event->beschreibung) ? $event->beschreibung : '';
}
else
{
$descriptionParts = array(
'Stundenplan',
isset($event->lehrfach_bez) ? $event->lehrfach_bez : '',
$lecturers,
$groups,
$location,
isset($event->beschreibung) ? $event->beschreibung : ''
);
$description = implode("\r\n", array_values(array_filter(
$descriptionParts,
function ($value) {
return $value !== '';
}
)));
}
return array(
'title' => $title,
'location' => $location,
'description' => $description,
'groups' => $groups,
'start_date' => $start->format('d.m.Y'),
'start_time' => $start->format('H:i:s'),
'end_date' => $end->format('d.m.Y'),
'end_time' => $end->format('H:i:s')
);
}
/**
* @param array $eventData Normalized event values.
* @return array Generic CSV row.
*/
private function buildGenericRow(array $eventData)
{
$category = defined('LVPLAN_KATEGORIE') ? LVPLAN_KATEGORIE : 'StundenplanTW';
return array(
$eventData['title'],
$category,
$eventData['location'],
$eventData['description'],
'Stundenplan',
$eventData['start_date'],
$eventData['start_time'],
$eventData['end_date'],
$eventData['end_time'],
'',
'',
'',
'',
''
);
}
/**
* @param array $eventData Normalized event values.
* @return array Outlook CSV row.
*/
private function buildOutlookRow(array $eventData)
{
$subject = $eventData['title'];
if ($eventData['groups'] !== '')
$subject .= ' - '.$eventData['groups'];
return array(
$subject,
$eventData['start_date'],
$eventData['start_time'],
$eventData['end_date'],
$eventData['end_time'],
'Aus',
'Aus',
'',
'',
'',
'',
'',
'',
'',
$eventData['description'],
'StundenplanFH',
$eventData['location'],
'Normal',
'Aus',
'',
'Normal',
'2'
);
}
/**
* @param object $event Calendar event.
* @return string Lecturer abbreviations.
*/
private function getLecturers($event)
{
$lecturers = array();
if (isset($event->lektor) && is_array($event->lektor))
{
foreach ($event->lektor as $lecturer)
{
if (isset($lecturer['kurzbz']) && $lecturer['kurzbz'] !== '')
$lecturers[] = $lecturer['kurzbz'];
}
}
return implode(' / ', array_unique($lecturers));
}
/**
* @param object $event Calendar event.
* @return string Participant group labels.
*/
private function getGroups($event)
{
$groups = array();
if (isset($event->gruppe) && is_array($event->gruppe))
{
foreach ($event->gruppe as $group)
{
if (isset($group['bezeichnung']) && $group['bezeichnung'] !== '')
$groups[] = $group['bezeichnung'];
}
}
if (isset($event->teilnehmer_gruppe) && is_array($event->teilnehmer_gruppe))
{
foreach ($event->teilnehmer_gruppe as $group)
{
if (isset($group['gruppe_kurzbz']) && $group['gruppe_kurzbz'] !== '')
$groups[] = $group['gruppe_kurzbz'];
}
}
return implode(' / ', array_unique($groups));
}
/**
* @param mixed $value String or list of strings.
* @param string $separator Join separator.
* @return string
*/
private function joinValues($value, $separator = ' / ')
{
return is_array($value) ? implode($separator, $value) : (string)$value;
}
/**
* Builds one fully quoted RFC-style CSV row.
*
* @param array $values Row values.
* @return string
*/
private function buildCSVLine(array $values)
{
$escapedValues = array_map(function ($value) {
$value = preg_replace('/\r\n|\r|\n/', "\r\n", (string)$value);
return '"'.str_replace('"', '""', $value).'"';
}, $values);
return implode(',', $escapedValues)."\r\n";
}
/**
* @param mixed $dateTime Date/time input.
* @return DateTime|null
*/
private function createDateTime($dateTime)
{
if ($dateTime instanceof DateTime)
return clone $dateTime;
if ($dateTime === null || $dateTime === '')
return null;
try
{
if (is_numeric($dateTime))
return new DateTime('@'.(int)$dateTime);
return new DateTime($dateTime);
}
catch (Exception $e)
{
return null;
}
}
}
@@ -80,10 +80,18 @@ export default {
+ '&pers_uid=' + encodeURIComponent(this.uid)
+ '&begin=' + encodeURIComponent(start)
+ '&ende=' + encodeURIComponent(ende);
const new_csv_download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ '/lvplan/csv/download'
+ '?type=' + encodeURIComponent(type)
+ '&pers_uid=' + encodeURIComponent(this.uid)
+ '&begin=' + encodeURIComponent(start)
+ '&ende=' + encodeURIComponent(ende);
return [
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
{ title: "csv-new", icon: 'fa-solid fa-file-csv', link: new_csv_download_link },
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' },
{ title: "ical1-new", icon: 'fa-regular fa-calendar', link: new_download_link + '&version=1' },