implemented coodle ical comp and corresponding endpoint

This commit is contained in:
adisposkofh
2026-07-22 16:03:29 +02:00
parent d89ea5947e
commit cb985c3c25
8 changed files with 218 additions and 2 deletions
+2
View File
@@ -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';
@@ -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;
}
}
+14 -1
View File
@@ -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);
}
}
@@ -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) {
+6
View File
@@ -108,4 +108,10 @@ export default {
},
};
},
getCoodleIcalLink() {
return {
method: "get",
url: "/api/frontend/v1/coodle/CoodleSurvey/getCoodleIcalUrl",
};
},
};
@@ -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*/ `
<div class="card" style="min-height:100%">
<div class="card-body">
<div class="d-flex flex-column gap-3">
<span>
{{ "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." }}
</span>
<span>
{{ "To exclude identifiable survey data: " }}
<a v-if="icalLink?.length" :href="icalLink" target="_blank">{{ icalLink }}</a>
</span>
<span>
{{ "To include identifiable survey data: " }}
<a v-if="encryptedIcalLink?.length" :href="encryptedIcalLink" target="_blank">{{ encryptedIcalLink }}</a>
</span>
</div>
</div>
</div>
`,
};
@@ -371,7 +371,6 @@ export default {
</div>
</div>
</div>
</div>
<coodle-survey-basic-info
v-model:surveyFormDataModelValue="surveyFormData"
:survey="$props.survey"
+13
View File
@@ -2,6 +2,7 @@ import ActiveCoodleSurveys from "./Components/ActiveCoodleSurveys.js";
import InactiveCoodleSurveys from "./Components/InactiveCoodleSurveys.js";
import CoodleSurvey from "./Components/CoodleSurvey.js";
import CoodleFreeBusy from "./Components/CoodleFreeBusy.js";
import CoodleIcal from "./Components/CoodleIcal.js";
import ApiAuthinfo from "../../../api/factory/authinfo.js";
import CoodleApi from "../../../api/factory/coodle.js";
@@ -13,6 +14,7 @@ export default {
InactiveCoodleSurveys,
CoodleSurvey,
CoodleFreeBusy,
CoodleIcal,
},
data() {
return {
@@ -141,6 +143,16 @@ export default {
</div>
<span class="text-nowrap">{{ "FreeBusy Settings" }}</span>
</div>
<div
@click="switchToTab('ical')"
:class="getTabStylingClass(view === 'ical')"
class="btn d-flex flex-row gap-2 align-items-center py-2"
>
<div>
<i class="fa-solid fa-calendar fa-lg"></i>
</div>
<span class="text-nowrap">{{ "Coodle iCal" }}</span>
</div>
</div>
</div>
<div class="flex-grow-1 mt-1">
@@ -161,6 +173,7 @@ export default {
:authInfo="authInfo"
/>
<coodle-free-busy v-else-if="view === 'freeBusySettings'" :authUid="authInfo?.uid" />
<coodle-ical v-else-if="view === 'ical'" :authUid="authInfo?.uid" />
</div>
</div>`,
};