diff options
| author | Simon Rettberg | 2025-07-01 10:33:28 +0200 |
|---|---|---|
| committer | Simon Rettberg | 2025-07-01 10:33:28 +0200 |
| commit | 87686da817dfd349c9f89a01065f799408e29f10 (patch) | |
| tree | 6e2a836b2bc9c4f6c45373d41ab700cba0690de8 | |
| parent | [locationinfo] icalparser: Undo the entity encoding before returning iCal data (diff) | |
| download | slx-admin-87686da817dfd349c9f89a01065f799408e29f10.tar.gz slx-admin-87686da817dfd349c9f89a01065f799408e29f10.tar.xz slx-admin-87686da817dfd349c9f89a01065f799408e29f10.zip | |
[locationinfo] Work around HisInOne returning incomplete iCal files
While the HisInOne help text says:
Die Permalinks zu Raumbelegungsplänen und Veranstaltungen enthalten den
Semesterbezug (currentTimeId oder periodId). Bei Bedarf entfernen Sie
diese Einschränkung inklusive "&" vorne und aller Zeichen dahinter.
This is evidently not true. Requesting the iCal URL without the periodId
leaves out random courses/events/lectures, and event requesting with the
current periodId might not give you the full results as displayed by the
web interface. Add crude brute-force approach that will request multiple
periodIds and merge all the lectures found.
3 files changed, 81 insertions, 13 deletions
diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php index 9128288d..92113f76 100644 --- a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php +++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php @@ -58,13 +58,76 @@ class CourseBackend_HisInOne extends ICalCourseBackend // Unfortunately HisInOne returns an internal server error if you pass an invalid roomId. // So we just try a bunch and see if anything works. Even if this fails, using // the backend should work, given the URL is actually correct. - foreach ([60, 100, 5, 10, 50, 110, 200, 210, 250, 300, 333, 500, 1000, 2000] as $roomId) { - if ($this->downloadIcal($roomId, 60) !== null) + foreach ([60, 100, 5, 10, 11, 12, 13, 14, 15, 50, 110, 200, 210, 250, 300, 333, 500, 1000, 2000] as $roomId) { + // Use parent as we don't care about the periodId shenanigans + if (parent::downloadIcal((string)$roomId, time() + 60) !== null) return true; } return false; } + protected function downloadIcal(string $roomId, ?int $deadline, ?string $appendToUrl = null, ?bool $limitRange = true): ?array + { + $events = parent::downloadIcal($roomId, $deadline, null, false); + if (empty($events)) + return $events; + $periodIds = []; + $finalEvents = []; + $dtStart = strtotime('-7 days'); + $dtEnd = strtotime('+7 days'); + foreach ($events as $event) { + if (isset($event->url) && preg_match('/[&?]periodId=([0-9]+)(&|$)/', $event->url, $out)) { + // Collect periodIds from URLs to find more lectures. While the HisInOne help says that the periodId + // parameter can simply be removed, in practice that means some lectures that take place even on the + // current day or week might not be returned in the resulting ical file, unless you pass the proper + // current periodId. Since there is no API or similar that reliably returns the current periodId, + // we just collect all the periodIds we find in the original ical file, and re-query with each of + // those appended to the URL, and merge the final result. + // Also, some courses seem to be assigned to past semesters, i.e. as of writing we have July 2025, + // but there is a lecture that won't be returned in the iCal data unless we make a request for + // the periodId corresponding to summer term 2024 (!) + $periodId = (int)$out[1]; + for ($i = min($periodId - 3, 1); $i <= $periodId + 3; $i++) { + $periodIds[$i] = $i; + } + } + if (strtotime($event->dtstart) > $dtEnd || strtotime($event->dtend) < $dtStart) + continue; + $finalEvents[$this->hash($event)] = $event; + } + // Now re-query with all the periodIds we found + $dupCheck = []; + while (($periodId = array_pop($periodIds)) !== null) { + if (isset($dupCheck[$periodId])) + continue; + if ($deadline !== null && $deadline - time() <= 1) + break; + $dupCheck[$periodId] = true; + $events = parent::downloadIcal($roomId, $deadline, '&periodId=' . $periodId); + if (empty($events)) + continue; + foreach ($events as $event) { + $finalEvents[$this->hash($event)] = $event; + // Collect more periodIds + if (isset($event->url) && preg_match('/[&?]periodId=([0-9]+)(&|$)/', $event->url, $out)) { + $periodId = (int)$out[1]; + for ($i = min($periodId - 3, 1); $i <= $periodId + 3; $i++) { + $periodIds[$i] = $i; + } + } + } + if (count($dupCheck) > 5) + break; + } + $this->addError("Queried periodIds for $roomId:" . implode(',', array_keys($dupCheck)), false); + return array_values($finalEvents); + } + + private function hash(ICalEvent $event): string + { + return md5($event->dtstart . $event->dtend . ($event->uid ?? $event->summary)); + } + public function getCacheTime(): int { return 30 * 60; diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php index 37d9d9d8..dbe53326 100644 --- a/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php +++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php @@ -15,7 +15,7 @@ class CourseBackend_ICal extends ICalCourseBackend $this->init($data['baseUrl'], $data['verifyCert'], $data['verifyHostname'], $data['authMethod'], $data['user'], $data['pass']); - $this->testId = $data['testId']; + $this->testId = (string)$data['testId']; return true; } @@ -39,7 +39,7 @@ class CourseBackend_ICal extends ICalCourseBackend return false; if (empty($this->testId)) return true; - return ($this->downloadIcal($this->testId, 60) !== null); + return ($this->downloadIcal($this->testId, time() + 60) !== null); } public function getCacheTime(): int diff --git a/modules-available/locationinfo/inc/icalcoursebackend.inc.php b/modules-available/locationinfo/inc/icalcoursebackend.inc.php index 6d9bbeef..de7ddb00 100644 --- a/modules-available/locationinfo/inc/icalcoursebackend.inc.php +++ b/modules-available/locationinfo/inc/icalcoursebackend.inc.php @@ -34,17 +34,21 @@ abstract class ICalCourseBackend extends CourseBackend } /** - * @param string $roomId room id - * @param callable $errorFunc - * @return ICalEvent[]|null all events for this room in the range -7 days to +7 days, or NULL on error + * Downloads iCal data for a specified room ID with optional deadline and URL appendage. + * + * @param string $roomId The ID of the room for which iCal data is to be downloaded. + * @param ?int $deadline Optional deadline timestamp for the download operation. + * @param ?string $appendToUrl Optional appendage to the URL used for the download. + * + * @return ?ICalEvent[] An array of iCal events if successful, or null on failure. */ - protected function downloadIcal(string $roomId, int $timeout): ?array + protected function downloadIcal(string $roomId, ?int $deadline, ?string $appendToUrl = null, ?bool $limitRange = true): ?array { if (!$this->isOK()) return null; try { - $ical = new ICalParser(['filterDaysBefore' => 7, 'filterDaysAfter' => 7]); + $ical = new ICalParser($limitRange ? ['filterDaysBefore' => 7, 'filterDaysAfter' => 7] : []); } catch (Exception $e) { $this->addError('Error instantiating ICalParser: ' . $e->getMessage(), true); return null; @@ -54,6 +58,8 @@ abstract class ICalCourseBackend extends CourseBackend $this->curlHandle = curl_init(); } + $timeout = ($deadline ?? PHP_INT_MAX) - time(); + $options = [ CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($ical) { $ical->feedData($data); @@ -62,7 +68,7 @@ abstract class ICalCourseBackend extends CourseBackend CURLOPT_FOLLOWLOCATION => true, CURLOPT_SSL_VERIFYHOST => $this->verifyHostname ? 2 : 0, CURLOPT_SSL_VERIFYPEER => $this->verifyCert ? 1 : 0, - CURLOPT_URL => str_replace('%ID%', $roomId, $this->location), + CURLOPT_URL => str_replace('%ID%', $roomId, $this->location . ($appendToUrl ?? '')), CURLOPT_TIMEOUT => min(60, $timeout), CURLOPT_CONNECTTIMEOUT => 4, ]; @@ -97,10 +103,9 @@ abstract class ICalCourseBackend extends CourseBackend } $tTables = []; foreach ($requestedRoomIds as $roomId) { - $timeout = ($deadline ?? PHP_INT_MAX) - time(); - if ($timeout <= 1) + if ($deadline !== null && $deadline - time() <= 1) break; - $data = $this->downloadIcal($roomId, $timeout); + $data = $this->downloadIcal($roomId, $deadline); if ($data === null) { $this->addError("Downloading ical for $roomId failed", false); continue; |
