From 028d4d2b07c50f2854d3cfc06f05855fa358ead3 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 19 Jan 2021 14:59:13 +0100 Subject: [locationinfo] Add generic ical backend Closes #3824 --- .../coursebackend/coursebackend_hisinone.inc.php | 109 +-------------- .../inc/coursebackend/coursebackend_ical.inc.php | 61 +++++++++ .../locationinfo/inc/icalcoursebackend.inc.php | 151 +++++++++++++++++++++ .../locationinfo/lang/de/backend-ical.json | 16 +++ .../locationinfo/lang/en/backend-ical.json | 16 +++ .../locationinfo/lang/en/template-tags.json | 2 +- 6 files changed, 252 insertions(+), 103 deletions(-) create mode 100644 modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php create mode 100644 modules-available/locationinfo/inc/icalcoursebackend.inc.php create mode 100644 modules-available/locationinfo/lang/de/backend-ical.json create mode 100644 modules-available/locationinfo/lang/en/backend-ical.json diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php index 82efae87..8bd18169 100644 --- a/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php +++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_hisinone.inc.php @@ -1,15 +1,7 @@ location = $this->mangleProperty('baseUrl', $data['baseUrl']); - $this->verifyHostname = $data['verifyHostname']; - $this->verifyCert = $data['verifyCert']; + $this->init($this->mangleProperty('baseUrl', $data['baseUrl']), + $data['verifyCert'], $data['verifyHostname']); return true; } @@ -39,14 +30,14 @@ class CourseBackend_HisInOne extends CourseBackend if ($prop === 'baseUrl') { // Update form SOAP to iCal url if (preg_match(',^(http.*?)/qisserver,', $value, $out)) { - $value = $out[1] . '/qisserver/pages/cm/exa/timetable/roomScheduleCalendarExport.faces?roomId='; + $value = $out[1] . '/qisserver/pages/cm/exa/timetable/roomScheduleCalendarExport.faces?roomId=%ID%'; } elseif (preg_match(',(.*[/=])\d*$', $value, $out)) { - $value = $out[1]; + $value = $out[1] . '%ID%'; } elseif (substr_count($value, '/') <= 3) { if (substr($value, -1) !== '/') { $value .= '/'; } - $value .= 'qisserver/pages/cm/exa/timetable/roomScheduleCalendarExport.faces?roomId='; + $value .= 'qisserver/pages/cm/exa/timetable/roomScheduleCalendarExport.faces?roomId=%ID%'; } } return $value; @@ -54,10 +45,8 @@ class CourseBackend_HisInOne extends CourseBackend public function checkConnection() { - if (empty($this->location)) { - $this->addError("Credentials are not set", true); + if (!$this->isOK()) return false; - } // 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. @@ -68,43 +57,6 @@ class CourseBackend_HisInOne extends CourseBackend return false; } - /** - * @param int $roomId room id - * @return ICalEvent[]|null all events for this room in the range -7 days to +7 days, or NULL on error - */ - private function downloadIcal($roomId) - { - if ($this->curlHandle === false) { - $this->curlHandle = curl_init(); - } - - $ical = new ICalParser(['filterDaysBefore' => 7, 'filterDaysAfter' => 7]); - $options = array( - CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($ical) { - $ical->feedData($data); - return strlen($data); - }, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_SSL_VERIFYHOST => $this->verifyHostname ? 2 : 0, - CURLOPT_SSL_VERIFYPEER => $this->verifyCert ? 1 : 0, - CURLOPT_URL => $this->location . $roomId, - CURLOPT_TIMEOUT => 60, - CURLOPT_CONNECTTIMEOUT => 4, - ); - - curl_setopt_array($this->curlHandle, $options); - - if (!curl_exec($this->curlHandle)) { - $this->addError('Curl error: ' . curl_error($this->curlHandle), false); - } - $ical->finish(); - if (!$ical->isValid()) { - $this->addError("Did not find a VCALENDAR in returned data for {$this->location}$roomId", false); - return null; - } - return $ical->events(); - } - public function getCacheTime() { return 30 * 60; @@ -121,51 +73,4 @@ class CourseBackend_HisInOne extends CourseBackend return "HisInOne"; } - public function fetchSchedulesInternal($requestedRoomIds) - { - if (empty($requestedRoomIds)) { - return array(); - } - $tTables = []; - foreach ($requestedRoomIds as $roomId) { - $data = $this->downloadIcal($roomId); - if ($data === null) { - $this->addError("Downloading ical for $roomId failed", false); - continue; - } - foreach ($data as $event) { - $tTables[$roomId][] = array( - 'title' => $this->toTitle($event), - 'start' => $event->dtstart, - 'end' => $event->dtend, - 'cancelled' => false, // ??? How - ); - } - } - return $tTables; - } - - /** - * Get a usable title from either SUMMARY or DESCRIPTION - * @param ICalEvent $event - */ - private function toTitle($event) - { - $title = $event->summary; - if (empty($title)) { - $title = $event->description; - } - if (empty($title)) { - $title = 'Unknown'; - } - return preg_replace([',(\s*\s*|\r|\n|\\\r|\\\n)+,', '/\\\\([,;:])/'], ["\n", '$1'], $title); - } - - public function __destruct() - { - if ($this->curlHandle !== false) { - curl_close($this->curlHandle); - } - } - } diff --git a/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php b/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php new file mode 100644 index 00000000..98dca1cb --- /dev/null +++ b/modules-available/locationinfo/inc/coursebackend/coursebackend_ical.inc.php @@ -0,0 +1,61 @@ +addError("No url is given", true); + return false; + } + + $this->init($data['baseUrl'], $data['verifyCert'], $data['verifyHostname'], $data['authMethod'], + $data['user'], $data['pass']); + $this->testId = $data['testId']; + + return true; + } + + public function getCredentialDefinitions() + { + return [ + new BackendProperty('baseUrl', 'string'), + new BackendProperty('verifyCert', 'bool', true), + new BackendProperty('verifyHostname', 'bool', true), + new BackendProperty('testId', 'string'), + new BackendProperty('authMethod', ['NONE', 'BASIC', 'DIGEST', 'GSSNEGOTIATE', 'NTLM'], 'NONE'), + new BackendProperty('user', 'string'), + new BackendProperty('pass', 'string'), + ]; + } + + public function checkConnection() + { + if (!$this->isOK()) + return false; + if (empty($this->testId)) + return true; + return ($this->downloadIcal($this->testId) !== null); + } + + public function getCacheTime() + { + return 30 * 60; + } + + public function getRefreshTime() + { + return 60 * 60; + } + + + public function getDisplayName() + { + return "iCal"; + } + +} diff --git a/modules-available/locationinfo/inc/icalcoursebackend.inc.php b/modules-available/locationinfo/inc/icalcoursebackend.inc.php new file mode 100644 index 00000000..fba0866c --- /dev/null +++ b/modules-available/locationinfo/inc/icalcoursebackend.inc.php @@ -0,0 +1,151 @@ +verifyCert = $verifyCert; + $this->verifyHostname = $verifyHostname; + if (strpos($location, '%ID%') === false) { + $location .= '%ID%'; + } + $this->location = $location; + $this->authMethod = $authMethod; + $this->user = $user; + $this->pass = $pass; + } + + /** + * @param int $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 + */ + protected function downloadIcal($roomId) + { + if (!$this->isOK()) + return null; + if ($this->curlHandle === false) { + $this->curlHandle = curl_init(); + } + + $ical = new ICalParser(['filterDaysBefore' => 7, 'filterDaysAfter' => 7]); + $options = [ + CURLOPT_WRITEFUNCTION => function ($ch, $data) use ($ical) { + $ical->feedData($data); + return strlen($data); + }, + 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_TIMEOUT => 60, + CURLOPT_CONNECTTIMEOUT => 4, + ]; + if ($this->authMethod !== 'NONE' && defined('CURLAUTH_' . $this->authMethod)) { + $options[CURLOPT_HTTPAUTH] = constant('CURLAUTH_' . $this->authMethod); + $options[CURLOPT_USERPWD] = $this->user; + if (!empty($this->pass)) { + $options[CURLOPT_USERPWD] .= ':' . $this->pass; + } + } + + curl_setopt_array($this->curlHandle, $options); + + $curlRet = curl_exec($this->curlHandle); + if (!$curlRet) { + $this->addError('Curl error: ' . curl_error($this->curlHandle) . ' for ' . $roomId, false); + } + $ical->finish(); + if (!$ical->isValid()) { + if ($curlRet) { + $this->addError("Did not find a VCALENDAR in returned data for $roomId", false); + } + return null; + } + return $ical->events(); + } + + public function fetchSchedulesInternal($requestedRoomIds): array + { + if (empty($requestedRoomIds) || !$this->isOK()) { + return array(); + } + $tTables = []; + foreach ($requestedRoomIds as $roomId) { + $data = $this->downloadIcal($roomId); + if ($data === null) { + $this->addError("Downloading ical for $roomId failed", false); + continue; + } + foreach ($data as $event) { + $tTables[$roomId][] = array( + 'title' => $this->toTitle($event), + 'start' => $event->dtstart, + 'end' => $event->dtend, + 'cancelled' => false, // ??? How + ); + } + } + return $tTables; + } + + /** + * Get a usable title from either SUMMARY or DESCRIPTION + * @param ICalEvent $event + */ + private function toTitle($event): string + { + $title = $event->summary; + if (empty($title)) { + $title = $event->description; + } + if (empty($title)) { + $title = 'Unknown'; + } + return (string)preg_replace([',(\s*\s*|\r|\n|\\\r|\\\n)+,', '/\\\\([,;:])/'], ["\n", '$1'], $title); + } + + protected function isOK(): bool + { + if (empty($this->location)) { + $this->addError("Credentials are not set", true); + return false; + } + return true; + } + + public function __destruct() + { + if ($this->curlHandle !== false) { + curl_close($this->curlHandle); + } + } + +} \ No newline at end of file diff --git a/modules-available/locationinfo/lang/de/backend-ical.json b/modules-available/locationinfo/lang/de/backend-ical.json new file mode 100644 index 00000000..9a91eb9f --- /dev/null +++ b/modules-available/locationinfo/lang/de/backend-ical.json @@ -0,0 +1,16 @@ +{ + "authMethod": "Authentifizierung", + "authMethod_helptext": "Falls eine Authentifizierung per HTTP-Header erforderlich ist, kann hier die gew\u00fcnschte Methode gew\u00e4hlt werden.", + "baseUrl": "Basis-URL", + "baseUrl_helptext": "URL zum iCal-File f\u00fcr diesen Raum. Ersetzen Sie den Part, der den Raum identifiziert durch %ID%, z.B. \"http:\/\/example.com\/calendars\/%ID%\". Die spezifische ID tragen Sie dann in der Raum\u00fcbersicht f\u00fcr jeden Raum individuell ein.", + "pass": "Passwort", + "pass_helptext": "Optional. Passwort f\u00fcr Authentifizierung.", + "testId": "Testraum", + "testId_helptext": "Optional. Tragen Sie hier eine g\u00fcltige Raum-ID f\u00fcr dieses Backend ein, um diese f\u00fcr Verbondungs-checks zu nutzen (Klick auf blauen \"Verbindung pr\u00fcfen\" Button in Backend-\u00dcbersicht).", + "user": "Nutzername f\u00fcr Authentifizierung", + "user_helptext": "Optional. Nutzername f\u00fcr Authentifizierung.", + "verifyCert": "Zertifikat pr\u00fcfen", + "verifyCert_helptext": "Wenn das Zertifikat abgelaufen ist, oder von keiner bekannten CA ausgestellt wurde, wird die Verbindung abgelehnt.", + "verifyHostname": "Hostnamen pr\u00fcfen", + "verifyHostname_helptext": "Der im Zertifikat angegebene Hostname muss mit dem Hostnamen aus der URL \u00fcbereinstimmen, sonst wird die Verbindung abgelehnt." +} \ No newline at end of file diff --git a/modules-available/locationinfo/lang/en/backend-ical.json b/modules-available/locationinfo/lang/en/backend-ical.json new file mode 100644 index 00000000..a5b26efd --- /dev/null +++ b/modules-available/locationinfo/lang/en/backend-ical.json @@ -0,0 +1,16 @@ +{ + "authMethod": "Athentication", + "authMethod_helptext": "If backend requires authentication via HTTP header, select appropriate method here.", + "baseUrl": "Base URL", + "baseUrl_helptext": "URL to iCal file for this room. Replace the part of the URL that identifies a specific room by %ID%, f.i. \"http:\/\/example.com\/locations\/%ID%\". Then switch to the \"location-specific settings\" tab and add the according ID to each room.", + "pass": "Password", + "pass_helptext": "Optional. Password for authentication.", + "testId": "Test room", + "testId_helptext": "Optional. Provide a valid room id for this backend for use when clicking the blue \"check connection\" button in the backend list.", + "user": "Username", + "user_helptext": "Optional. Username for authentication.", + "verifyCert": "Verify certificate", + "verifyCert_helptext": "If the certificate expired or was not signed by a known CA, the connection will be aborted.", + "verifyHostname": "Verify host name", + "verifyHostname_helptext": "The certificate's host name must match the host name given in the URL, otherwise the connection will be aborted." +} \ No newline at end of file diff --git a/modules-available/locationinfo/lang/en/template-tags.json b/modules-available/locationinfo/lang/en/template-tags.json index 5a87e42c..ce0eac98 100644 --- a/modules-available/locationinfo/lang/en/template-tags.json +++ b/modules-available/locationinfo/lang/en/template-tags.json @@ -55,7 +55,7 @@ "lang_languageTooltip": "The language the frontend uses", "lang_lastCalendarUpdate": "Calendar update", "lang_locationName": "Name", - "lang_locationSettings": "Location specific settings", + "lang_locationSettings": "Location-specific settings", "lang_locations": "Locations", "lang_locationsTable": "Rooms \/ Locations", "lang_locationsTableHints": "Here you can link the location ID to a configured backend (e.g. HISinOne) to show calendar events.", -- cgit v1.2.3-55-g7522