web - refactor core modules, harden authorization, simplify endpoints

This commit is contained in:
2026-08-29 11:15:54 +02:00
parent e1109a07ef
commit 79d857d686
71 changed files with 8269 additions and 9119 deletions
@@ -33,7 +33,7 @@ class lib_Feiertage_Connector
return $wert === $datum;
});
if ($filter !== null) {
if (!empty($filter)) {
$feiertagsname = implode(" ",array_keys($filter));
}
@@ -46,7 +46,7 @@ class lib_Feiertage_Connector
if(isset(self::$cache['c0-'.$jahr]))
return self::$cache['c0-'.$jahr];
$file = "data/feiertage_$jahr.json";
$file = __DIR__ . "/../data/feiertage_$jahr.json";
if (!file_exists($file)) { //Ggf. Datei von externer API speichern
@@ -63,7 +63,7 @@ class lib_Feiertage_Connector
if(isset(self::$cache['c0-'.$jahr.$land]))
return self::$cache['c0-'.$jahr.$land];
$file = "data/feiertage_" . $jahr . "_" . $land . ".json";
$file = __DIR__ . "/../data/feiertage_" . $jahr . "_" . $land . ".json";
if (!file_exists($file)) { //Ggf. Datei von externer API speichern
@@ -99,7 +99,7 @@ class lib_Feiertage_Connector
if(isset(self::$cache['c1-'.$jahr]))
return self::$cache['c1-'.$jahr];
$file = "data/ferientage_berlin_$jahr.json";
$file = __DIR__ . "/../data/ferientage_berlin_$jahr.json";
if (!file_exists($file)) { //Ggf. Datei von externer API speichern
@@ -0,0 +1,446 @@
<?php
/* Gemeinsame, seiteneffektarme Hilfsfunktionen des Seelsorgeplaners. */
if (!defined('SEELSORGE_INITIAL_VISIT_DATE')) {
define('SEELSORGE_INITIAL_VISIT_DATE', '2024-07-11');
}
function seelsorge_csrf_token(): string
{
if (empty($_SESSION['seelsorge_csrf'])) {
$_SESSION['seelsorge_csrf'] = bin2hex(random_bytes(32));
}
return (string) $_SESSION['seelsorge_csrf'];
}
function seelsorge_verify_csrf(array $input): void
{
$postedToken = $input['csrf_token'] ?? '';
$sessionToken = $_SESSION['seelsorge_csrf'] ?? '';
if (!is_string($postedToken) || !is_string($sessionToken) || $sessionToken === '' || !hash_equals($sessionToken, $postedToken)) {
throw new InvalidArgumentException('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
}
}
function seelsorge_profile_image_url(int $userId): string
{
$default = '/intranet_repo/general_files/profile_pictures/default.jpg';
if ($userId <= 0 || empty($_SERVER['DOCUMENT_ROOT'])) {
return $default;
}
$images = glob($_SERVER['DOCUMENT_ROOT'] . "/intranet_repo/general_files/profile_pictures/{$userId}-*.jpg") ?: [];
if (!$images) {
return $default;
}
usort($images, static fn($a, $b) => filemtime($b) <=> filemtime($a));
return str_replace($_SERVER['DOCUMENT_ROOT'], '', $images[0]);
}
function seelsorge_load_priests(PDO $pdo): array
{
$stmt = $pdo->query(
'SELECT u.user_id, u.first_name, u.last_name, u.mail AS email
FROM user_general_information u
JOIN user_permissions p ON p.user_id = u.user_id
WHERE p."PriesterIn" = 1
ORDER BY u.last_name ASC, u.first_name ASC'
);
return $stmt ? $stmt->fetchAll() : [];
}
function seelsorge_priest_options(array $priests): string
{
$options = [];
foreach ($priests as $priest) {
$userId = (int) ($priest['user_id'] ?? 0);
$firstName = trim((string) ($priest['first_name'] ?? ''));
$lastName = trim((string) ($priest['last_name'] ?? ''));
$email = trim((string) ($priest['email'] ?? ''));
if ($firstName !== '' && $lastName !== '') {
$label = $lastName . ', ' . $firstName;
} elseif ($lastName !== '' || $firstName !== '') {
$label = $lastName !== '' ? $lastName : $firstName;
} else {
$label = $email !== '' ? $email : "User #{$userId}";
}
$options[] = sprintf(
'<option value="%d">%s</option>',
$userId,
htmlspecialchars($label, ENT_QUOTES, 'UTF-8')
);
}
return implode('', $options);
}
function seelsorge_parse_visit_entry(array $input): array
{
$priestId = trim((string) ($input['pr_bezirk'] ?? ''));
$entry = [
'besuch_id' => max(0, (int) ($input['besuch_id'] ?? 0)),
'last_name' => trim((string) ($input['last_name'] ?? '')),
'first_name' => trim((string) ($input['first_name'] ?? '')),
'birth_date' => trim((string) ($input['birth_date'] ?? '')),
'address_street' => trim((string) ($input['address_street'] ?? '')),
'address_plz' => trim((string) ($input['address_plz'] ?? '')),
'address_link' => trim((string) ($input['address_link'] ?? '')),
'phone' => preg_replace('/\D+/', '', trim((string) ($input['phone'] ?? ''))) ?: null,
'comment' => trim((string) ($input['comment'] ?? '')),
'letzter_besuch' => trim((string) ($input['letzter_besuch'] ?? '')) ?: SEELSORGE_INITIAL_VISIT_DATE,
'priority' => (int) ($input['priority'] ?? 0),
'pr_bezirk' => ctype_digit($priestId) ? (int) $priestId : 0,
];
if ($entry['last_name'] === '' || $entry['first_name'] === '' || $entry['address_street'] === ''
|| $entry['address_plz'] === '' || $entry['address_link'] === '' || $entry['pr_bezirk'] <= 0) {
throw new InvalidArgumentException('Pflichtfelder fehlen.');
}
foreach (['birth_date', 'letzter_besuch'] as $field) {
$date = DateTime::createFromFormat('!Y-m-d', $entry[$field]);
if (!$date || $date->format('Y-m-d') !== $entry[$field]) {
throw new InvalidArgumentException('Ungültiges Datum.');
}
}
if ($entry['priority'] < 0 || $entry['priority'] > 2) {
throw new InvalidArgumentException('Ungültige Priorität.');
}
return $entry;
}
function seelsorge_visit_entry_client_data(array $row): array
{
return [
'besuch_id' => (int) ($row['besuch_id'] ?? 0),
'last_name' => $row['last_name'] ?? '',
'first_name' => $row['first_name'] ?? '',
'birth_date' => $row['birth_date'] ?? '',
'address_street' => $row['address_street'] ?? '',
'address_plz' => $row['address_plz'] ?? '',
'address_link' => $row['address_link'] ?? '',
'phone' => $row['phone'] ?? '',
'comment' => $row['comment'] ?? '',
'letzter_besuch' => $row['letzter_besuch'] ?? '',
'priority' => (int) ($row['priority'] ?? 0),
'pr_bezirk' => $row['pr_bezirk'] === null ? '' : (int) $row['pr_bezirk'],
'reservation' => $row['reservation'] ?? null,
];
}
function seelsorge_changed_visit_fields(array $oldEntry, array $newEntry): array
{
$labels = [
'last_name' => 'Nachname',
'first_name' => 'Vorname',
'birth_date' => 'Geburtsdatum',
'address_street' => 'Adresse',
'address_plz' => 'PLZ/Ort',
'address_link' => 'Karten-Link',
'phone' => 'Telefon',
'comment' => 'Kommentar',
'letzter_besuch' => 'Letzter Besuch',
'priority' => 'Priorität',
'pr_bezirk' => 'Priester-Bezirk',
];
$changed = [];
foreach ($labels as $field => $label) {
if ((string) ($oldEntry[$field] ?? '') !== (string) ($newEntry[$field] ?? '')) {
$changed[] = $label;
}
}
return $changed;
}
function seelsorge_save_visit_entry(PDO $pdo, array $entry): void
{
$fields = [
'last_name', 'first_name', 'birth_date', 'address_street', 'address_plz',
'address_link', 'phone', 'comment', 'letzter_besuch', 'priority', 'pr_bezirk',
];
$values = array_map(static fn($field) => $entry[$field], $fields);
$visitId = (int) $entry['besuch_id'];
$pdo->beginTransaction();
try {
if ($visitId === 0) {
$stmt = $pdo->prepare(
'INSERT INTO seelsorge_besuchsstellen
(last_name, first_name, birth_date, address_street, address_plz, address_link, phone, comment, letzter_besuch, priority, pr_bezirk, reservation)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)'
);
$stmt->execute($values);
$pdo->commit();
return;
}
$stmtOld = $pdo->prepare(
'SELECT last_name, first_name, birth_date, address_street, address_plz, address_link, phone, comment, letzter_besuch, priority, pr_bezirk
FROM seelsorge_besuchsstellen WHERE besuch_id = ?'
);
$stmtOld->execute([$visitId]);
$oldEntry = $stmtOld->fetch();
if (!$oldEntry) {
throw new RuntimeException('Eintrag nicht gefunden.');
}
$stmt = $pdo->prepare(
'UPDATE seelsorge_besuchsstellen
SET last_name = ?, first_name = ?, birth_date = ?, address_street = ?, address_plz = ?,
address_link = ?, phone = ?, comment = ?, letzter_besuch = ?, priority = ?, pr_bezirk = ?
WHERE besuch_id = ?'
);
$stmt->execute(array_merge($values, [$visitId]));
$changedFields = seelsorge_changed_visit_fields($oldEntry, $entry);
if ($changedFields) {
$personName = ($oldEntry['last_name'] ?? $entry['last_name']) . ', ' . ($oldEntry['first_name'] ?? $entry['first_name']);
$stmtLog = $pdo->prepare(
"INSERT INTO admin_messages
(type, sender_name, sender_email, subject, message, status, processed_at, created_at)
VALUES ('system', 'Seelsorgeplaner', '', ?, ?, 'done', NOW(), NOW())"
);
$stmtLog->execute([
"Besuchsstelle geändert: {$personName}",
"Besuchsstelle #{$visitId} ({$personName}) wurde geändert. Geänderte Felder: " . implode(', ', $changedFields) . '.',
]);
}
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}
function seelsorge_visit_action(PDO $pdo, string $action, int $visitId, int $userId): void
{
if ($visitId <= 0) {
throw new InvalidArgumentException('Ungültige Besuchs-ID.');
}
if ($action === 'delete') {
$stmt = $pdo->prepare('DELETE FROM seelsorge_besuchsstellen WHERE besuch_id = ?');
$stmt->execute([$visitId]);
if ($stmt->rowCount() === 0) {
throw new RuntimeException('Eintrag nicht gefunden.');
}
return;
}
if ($action === 'prioritize') {
$stmt = $pdo->prepare('UPDATE seelsorge_besuchsstellen SET priority = 2 WHERE besuch_id = ?');
$stmt->execute([$visitId]);
if ($stmt->rowCount() === 0) {
throw new RuntimeException('Eintrag nicht gefunden.');
}
return;
}
$stmt = $pdo->prepare('SELECT address_street, address_plz FROM seelsorge_besuchsstellen WHERE besuch_id = ?');
$stmt->execute([$visitId]);
$address = $stmt->fetch();
if (!$address) {
throw new RuntimeException('Eintrag nicht gefunden.');
}
$params = [];
switch ($action) {
case 'reserve':
$sql = 'UPDATE seelsorge_besuchsstellen SET reservation = ? WHERE address_street = ? AND address_plz = ?';
$params[] = $userId;
break;
case 'unreserve':
$sql = 'UPDATE seelsorge_besuchsstellen SET reservation = NULL WHERE address_street = ? AND address_plz = ?';
break;
case 'complete':
$sql = 'UPDATE seelsorge_besuchsstellen SET letzter_besuch = CURRENT_DATE, priority = 0, reservation = NULL WHERE address_street = ? AND address_plz = ?';
break;
case 'update_date':
$sql = 'UPDATE seelsorge_besuchsstellen SET letzter_besuch = CURRENT_DATE, priority = 0 WHERE address_street = ? AND address_plz = ?';
break;
default:
throw new InvalidArgumentException('Unbekannte Aktion.');
}
$stmt = $pdo->prepare($sql);
$stmt->execute(array_merge($params, [$address['address_street'], $address['address_plz']]));
}
function seelsorge_process_visit_command(PDO $pdo, array $input, int $userId): void
{
$command = trim((string) ($input['visit_command'] ?? ''));
if ($command === 'save') {
seelsorge_save_visit_entry($pdo, seelsorge_parse_visit_entry($input));
return;
}
seelsorge_visit_action($pdo, $command, (int) ($input['besuch_id'] ?? 0), $userId);
}
function seelsorge_toggle_availability(PDO $pdo, string $date, int $userId): bool
{
$stmt = $pdo->prepare(
'WITH deleted AS (
DELETE FROM verfuegbarkeiten WHERE datum = ? AND user_id = ? RETURNING user_id
)
INSERT INTO verfuegbarkeiten (datum, user_id)
SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM deleted)
RETURNING user_id'
);
$stmt->execute([$date, $userId, $date, $userId]);
return $stmt->fetchColumn() !== false;
}
function seelsorge_notify_match_by_mail(array $users, DateTimeInterface $date): void
{
if (count($users) < 2) {
return;
}
$names = array_values(array_filter(array_map(
static fn($user) => trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')),
$users
)));
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/ini/send_mail.php';
$formattedDate = $date->format('d.m.Y');
$subject = "Seelsorge-Match am {$formattedDate}";
$message = "It's a match!\n\n" . implode(', ', $names) . " haben am {$formattedDate} für einen Seelsorgebesuch Zeit.\n\nBitte stimmt euch untereinander ab.";
$host = $_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN');
$plannerUrl = "https://{$host}/intranet_repo/projects/seelsorge/seelsorge.php";
foreach ($users as $user) {
$email = trim((string) ($user['email'] ?? ''));
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
continue;
}
if (!send_intranet_mail($email, $subject, $message, null, 'intranet@nak-reinickendorf.de', 'Zum Seelsorgeplaner', $plannerUrl)) {
error_log("Seelsorge match mail failed for user " . (int) ($user['user_id'] ?? 0));
}
}
}
function seelsorge_calendar_availability_map(PDO $pdo, string $startDate, string $endDate, int $userId): array
{
$stmt = $pdo->prepare(
'SELECT datum, COUNT(*) AS count,
SUM(CASE WHEN user_id = ? THEN 1 ELSE 0 END) AS self_count
FROM verfuegbarkeiten
WHERE datum BETWEEN ? AND ?
GROUP BY datum'
);
$stmt->execute([$userId, $startDate, $endDate]);
$map = [];
foreach ($stmt->fetchAll() as $row) {
$date = (string) $row['datum'];
$map[$date] = [
'count' => (int) $row['count'],
'self' => (int) $row['self_count'] > 0,
];
}
return $map;
}
function seelsorge_group_households(array $entries): array
{
$households = [];
foreach ($entries as $entry) {
$key = ($entry['address_street'] ?? '') . '||' . ($entry['address_plz'] ?? '');
if (!isset($households[$key])) {
$households[$key] = [
'members' => [],
'address_street' => $entry['address_street'] ?? '',
'address_plz' => $entry['address_plz'] ?? '',
'address_link' => $entry['address_link'] ?? '',
'phone' => $entry['phone'] ?? '',
'reservation' => null,
];
}
$households[$key]['members'][] = $entry;
if (($entry['reservation'] ?? null) !== null) {
$households[$key]['reservation'] = $entry['reservation'];
}
}
return $households;
}
function seelsorge_household_status(array $members, bool $isReserved, ?int $todayTimestamp = null): array
{
if ($isReserved) {
return ['key' => 'blue', 'label' => 'Geplant'];
}
$todayTimestamp = $todayTimestamp ?? strtotime(date('Y-m-d'));
$status = 'green';
foreach ($members as $member) {
$lastVisit = strtotime((string) ($member['letzter_besuch'] ?? SEELSORGE_INITIAL_VISIT_DATE));
$months = (int) floor(($todayTimestamp - $lastVisit) / (86400 * 30));
$priority = (int) ($member['priority'] ?? 0);
if ($priority === 2 || $months >= 6) {
$status = 'red';
break;
}
if ($priority === 1 || $months >= 2) {
$status = 'orange';
}
}
$labels = ['red' => 'Dringend', 'orange' => 'Fällig', 'green' => 'Aktuell'];
return ['key' => $status, 'label' => $labels[$status]];
}
function seelsorge_relative_visit_text(string $lastVisit, ?int $todayTimestamp = null): string
{
if ($lastVisit === SEELSORGE_INITIAL_VISIT_DATE) {
return 'Initialer Eintrag';
}
$todayTimestamp = $todayTimestamp ?? strtotime(date('Y-m-d'));
$days = max(0, (int) floor(($todayTimestamp - strtotime($lastVisit)) / 86400));
if ($days >= 25) {
$months = (int) floor($days / 30);
return $months === 1 ? 'vor einem Monat' : "vor {$months} Monaten";
}
if ($days >= 7) {
$weeks = (int) floor($days / 7);
return $weeks === 1 ? 'vor einer Woche' : "vor {$weeks} Wochen";
}
return $days === 1 ? 'vor einem Tag' : "vor {$days} Tagen";
}
function seelsorge_profile_records(PDO $pdo): array
{
static $cache = null;
if ($cache !== null) {
return $cache;
}
$users = $pdo->query('SELECT * FROM user_general_information ORDER BY user_id')->fetchAll();
$permissionRows = $pdo->query('SELECT * FROM user_permissions')->fetchAll();
$permissionsByUser = [];
foreach ($permissionRows as $permissions) {
$permissionsByUser[(int) $permissions['user_id']] = $permissions;
}
$cache = [];
foreach ($users as $user) {
$userId = (int) $user['user_id'];
$cache[] = [
'user' => $user,
'permissions' => $permissionsByUser[$userId] ?? [],
'image' => seelsorge_profile_image_url($userId),
];
}
return $cache;
}