diff --git a/application/config/routes.php b/application/config/routes.php index a161a17a5..95f27881c 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -71,6 +71,8 @@ $route['Cis/StgOrgLvPlan/.*'] = 'Cis/StgOrgLvPlan/index/$1'; $route['Cis/Coodle'] = 'Cis/Coodle'; $route['CoodleFreeBusy/(:any)'] = 'api/frontend/v1/coodle/FreeBusy/getCoodleFreeBusy/$1'; +$route['CoodleIcal/encrypted/(:any)'] = 'api/frontend/v1/coodle/CoodleSurvey/getCoodleIcalEncrypted/$1'; +$route['CoodleIcal/(:any)'] = 'api/frontend/v1/coodle/CoodleSurvey/getCoodleIcal/$1'; $route['Abgabetool/Assistenz'] = 'Cis/Abgabetool/Assistenz'; $route['Abgabetool/Assistenz/(:any)'] = 'Cis/Abgabetool/Assistenz/$1'; diff --git a/application/controllers/api/frontend/v1/coodle/CoodleSurvey.php b/application/controllers/api/frontend/v1/coodle/CoodleSurvey.php index 4b8cac211..70e24baf4 100644 --- a/application/controllers/api/frontend/v1/coodle/CoodleSurvey.php +++ b/application/controllers/api/frontend/v1/coodle/CoodleSurvey.php @@ -23,6 +23,8 @@ class CoodleSurvey extends FHCAPI_Controller { protected $coodlePageUrl; + protected $coodleIcalUrl; + protected $coodleIcalUrlWithEncryptedUid; /** * Object initialization @@ -40,9 +42,13 @@ class CoodleSurvey extends FHCAPI_Controller 'cancelSurvey' => self::PERM_LOGGED, 'completeSurvey' => self::PERM_LOGGED, 'sendVotingReminders' => self::PERM_LOGGED, + 'getCoodleIcalUrl' => self::PERM_LOGGED, + 'getCoodleIcal' => self::PERM_ANONYMOUS, + 'getCoodleIcalEncrypted' => self::PERM_ANONYMOUS, ]); $this->load->library('form_validation'); + $this->load->library('CryptLib'); $this->load->model('person/Person_model', 'PersonModel'); $this->load->model('ressource/CoodleSurvey_model', 'CoodleSurveyModel'); $this->load->model('ressource/CoodleSurveyTimeslot_model', 'CoodleSurveyTimeslotModel'); @@ -51,6 +57,8 @@ class CoodleSurvey extends FHCAPI_Controller $this->load->helper('hlp_sancho_helper'); $this->coodlePageUrl = APP_ROOT . "cis.php/Cis/Coodle"; + $this->coodleIcalUrl = APP_ROOT . "cis.php/CoodleIcal/{uid}"; + $this->coodleIcalUrlWithEncryptedUid = APP_ROOT . "cis.php/CoodleIcal/encrypted/{encryptedUid}"; } //------------------------------------------------------------------------------------------------------------------ @@ -726,6 +734,44 @@ class CoodleSurvey extends FHCAPI_Controller $this->terminateWithSuccess(); } + public function getCoodleIcalUrl() + { + $uid = getAuthUID(); + + $encryptedUid = $this->cryptlib->RIJNDAEL_256_ECB(str_pad($uid, 32, chr(0)), LVPLAN_CYPHER_KEY, true); + $encryptedUid = base64_encode($encryptedUid); + $encryptedUid = str_replace(array('+', '/', '='), array('-', '_', ''), $encryptedUid); + $encryptedUid = trim($encryptedUid); + + $unencryptedUrl = str_replace("{uid}", $uid, $this->coodleIcalUrl); + $encryptedUrl = str_replace("{encryptedUid}", $encryptedUid, $this->coodleIcalUrlWithEncryptedUid); + + $this->terminateWithSuccess([ + "unencryptedUrl" => $unencryptedUrl, + "encryptedUrl" => $encryptedUrl, + ]); + } + + public function getCoodleIcal($uid) + { + $coodleIcalFilePath = $this->_getCoodleIcal($uid, false); + $this->terminateWithFileOutput("text/calendar", file_get_contents($coodleIcalFilePath), "coodle_ical.ics"); + } + + public function getCoodleIcalEncrypted($encryptedUid) + { + $data = str_replace(array('-', '_'), array('+', '/'), $encryptedUid); + $mod4 = strlen($data) % 4; + if ($mod4) { + $data .= substr('====', $mod4); + } + $decodedData = base64_decode($data); + + $uid = trim($this->cryptlib->RIJNDAEL_256_ECB_DECRYPT($decodedData, LVPLAN_CYPHER_KEY, true)); + + $coodleIcalFilePath = $this->_getCoodleIcal($uid, true); + $this->terminateWithFileOutput("text/calendar", file_get_contents($coodleIcalFilePath), "coodle_ical.ics"); + } // ----------------------------------------------------------------------------------------------------------------- // Private methods @@ -865,4 +911,68 @@ class CoodleSurvey extends FHCAPI_Controller fwrite($calendarFile, "END:VEVENT" . PHP_EOL); fwrite($calendarFile, "END:VCALENDAR"); } + + private function _getCoodleIcal($uid, $shouldIncludeIdentifyingInformation) + { + $activeParticipantEntries = $this->CoodleSurveyParticipantModel->getActiveParticipantEntriesByUid($uid); + $selectedTimeslotIds = []; + foreach ($activeParticipantEntries as $participantEntry) { + if ($participantEntry->selection === null) + continue; + + $selectedTimeslotIds = array_merge($selectedTimeslotIds, json_decode($participantEntry->selection)); + } + + $events = []; + $localTimezone = new DateTimeZone("Europe/Vienna"); + $utcTimezone = new DateTimeZone("UTC"); + foreach ($selectedTimeslotIds as $timeslotId) { + $timeslot = $this->CoodleSurveyTimeslotModel->getTimeslot($timeslotId); + if (!$timeslot) + continue; + + $survey = $this->CoodleSurveyModel->getSurvey($timeslot->survey_id); + + $startTime = new DateTime($timeslot->starts_at, $localTimezone); + $endTime = new DateTime($timeslot->starts_at, $localTimezone); + $endTime->modify("+$survey->timeslot_duration minutes"); + + $startTime->setTimezone($utcTimezone); + $endTime->setTimezone($utcTimezone); + + $surveyCreatorFullName = getData($this->PersonModel->getFullName($survey->creator_uid)); + + $formattedStartTime = $startTime->format("Ymd\THis\Z"); + $formattedEndTime = $endTime->format("Ymd\THis\Z"); + $events[] = [ + "id" => "coodle_termin_option_" . $survey->id . "_" . $formattedStartTime . "_" . $formattedEndTime, + "summary" => "Coodle Terminoption" . ($shouldIncludeIdentifyingInformation ? " $survey->title" : ""), + "description" => $shouldIncludeIdentifyingInformation ? "Erstellt von " . $surveyCreatorFullName : "", + "start" => $formattedStartTime, + "end" => $formattedEndTime, + ]; + } + + $coodleIcalFilePath = tempnam(sys_get_temp_dir(), "coodle_ical_"); + + $coodleIcalFile = fopen($coodleIcalFilePath, "w"); + fwrite($coodleIcalFile, "BEGIN:VCALENDAR" . PHP_EOL); + fwrite($coodleIcalFile, "VERSION:2.0" . PHP_EOL); + fwrite($coodleIcalFile, "PRDODID:" . CAMPUS_NAME . PHP_EOL); + + foreach ($events as $event) { + fwrite($coodleIcalFile, "BEGIN:VEVENT" . PHP_EOL); + fwrite($coodleIcalFile, "UID:" . $event["id"] . PHP_EOL); + fwrite($coodleIcalFile, "SUMMARY:" . $event["summary"] . PHP_EOL); + fwrite($coodleIcalFile, "DESCRIPTION:" . $event["description"] . PHP_EOL); + fwrite($coodleIcalFile, "DTSTART:" . $event["start"] . PHP_EOL); + fwrite($coodleIcalFile, "DTNED:" . $event["end"] . PHP_EOL); + fwrite($coodleIcalFile, "TRANS:OPAQUE" . PHP_EOL); + fwrite($coodleIcalFile, "END:VEVENT" . PHP_EOL); + } + + fwrite($coodleIcalFile, "END:VCALENDAR"); + + return $coodleIcalFilePath; + } } diff --git a/application/libraries/CryptLib.php b/application/libraries/CryptLib.php index ea19f3e5b..2f2b1e611 100644 --- a/application/libraries/CryptLib.php +++ b/application/libraries/CryptLib.php @@ -25,4 +25,17 @@ class CryptLib return $cipher->encrypt($value); } -} + + public static function RIJNDAEL_256_ECB_DECRYPT($value, $key, $paddingDisabled = false) + { + if (isEmptyString($value) || isEmptyString($key)) return null; + + $cipher = new Rijndael(Rijndael::MODE_ECB); + $cipher->setBlockLength(256); + $cipher->setKey($key); + + if ($paddingDisabled === true) $cipher->disablePadding(); + + return $cipher->decrypt($value); + } +} \ No newline at end of file diff --git a/application/models/ressource/CoodleSurveyParticipant_model.php b/application/models/ressource/CoodleSurveyParticipant_model.php index 8553b6148..775a77af7 100644 --- a/application/models/ressource/CoodleSurveyParticipant_model.php +++ b/application/models/ressource/CoodleSurveyParticipant_model.php @@ -32,6 +32,20 @@ class CoodleSurveyParticipant_model extends DB_Model return $this->execQuery($query)->retval; } + public function getActiveParticipantEntriesByUid($uid) + { + $query = "SELECT + surveyParticipant.survey_id as survey_id, + surveyParticipant.participant_uid as participant_uid, + surveyParticipant.selection as selection + FROM $this->dbTable surveyParticipant + JOIN campus.tbl_coodle_surveys survey ON(survey.id = surveyParticipant.survey_id) + WHERE surveyParticipant.participant_uid = '$uid' + AND survey.completed_at IS NULL + AND survey.canceled_at IS NULL"; + return $this->execQuery($query)->retval; + } + public function updateParticipants($surveyId, $participants, $timeslots) { $participantUids = array_map(function ($participant) { diff --git a/public/js/api/factory/coodle.js b/public/js/api/factory/coodle.js index 58937f99c..b6f6c3aa4 100644 --- a/public/js/api/factory/coodle.js +++ b/public/js/api/factory/coodle.js @@ -108,4 +108,10 @@ export default { }, }; }, + getCoodleIcalLink() { + return { + method: "get", + url: "/api/frontend/v1/coodle/CoodleSurvey/getCoodleIcalUrl", + }; + }, }; diff --git a/public/js/components/Cis/Coodle/Components/CoodleIcal.js b/public/js/components/Cis/Coodle/Components/CoodleIcal.js new file mode 100644 index 000000000..378f9ddc5 --- /dev/null +++ b/public/js/components/Cis/Coodle/Components/CoodleIcal.js @@ -0,0 +1,59 @@ +import CoodleApi from "../../../../api/factory/coodle.js"; + +export default { + name: "CoodleIcal", + props: { + authUid: String | null, + }, + data() { + return { + icalLink: "", + encryptedIcalLink: "", + }; + }, + computed: { + isDarkMode() { + return this.$theme.theme_name.value == "dark"; + }, + link() { + return this.icalLink; + }, + }, + methods: { + async getIcalLink() { + const icalLinkResponse = await this.$api.call(CoodleApi.getCoodleIcalLink()); + if (icalLinkResponse.meta.status === "success") { + this.icalLink = icalLinkResponse.data.unencryptedUrl; + this.encryptedIcalLink = icalLinkResponse.data.encryptedUrl; + console.log(icalLinkResponse); + console.log(this.icalLink); + console.log(this.encryptedIcalLink); + } + }, + }, + async created() { + await this.getIcalLink(); + console.log(this.icalLink); + console.log(this.encryptedIcalLink); + }, + template: /*html*/ ` +
+
+
+ + {{ "You can integrate your selections from active Coodle surveys into your preferred calendar application. To do so, import the .ics file from one of the following links." }} + + + {{ "To exclude identifiable survey data: " }} + {{ icalLink }} + + + {{ "To include identifiable survey data: " }} + {{ encryptedIcalLink }} + + +
+
+
+ `, +}; diff --git a/public/js/components/Cis/Coodle/Components/CoodleSurvey.js b/public/js/components/Cis/Coodle/Components/CoodleSurvey.js index f9d4e9802..5d1cb8da5 100644 --- a/public/js/components/Cis/Coodle/Components/CoodleSurvey.js +++ b/public/js/components/Cis/Coodle/Components/CoodleSurvey.js @@ -371,7 +371,6 @@ export default { - {{ "FreeBusy Settings" }} +
+
+ +
+ {{ "Coodle iCal" }} +
@@ -161,6 +173,7 @@ export default { :authInfo="authInfo" /> +
`, };