summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Rettberg2025-07-01 10:33:28 +0200
committerSimon Rettberg2025-07-01 10:33:28 +0200
commit87686da817dfd349c9f89a01065f799408e29f10 (patch)
tree6e2a836b2bc9c4f6c45373d41ab700cba0690de8
parent[locationinfo] icalparser: Undo the entity encoding before returning iCal data (diff)
downloadslx-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.
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php67
-rw-r--r--modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php4
-rw-r--r--modules-available/locationinfo/inc/icalcoursebackend.inc.php23
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;