Drei zusammenhaengende Punkte aus der Analyse des Login-Problems. 1. PHP-Fehler wurden an Besucher ausgegeben display_errors war in impfwarteliste.php (oeffentlich), intern/ impfwarteliste.php, intern/neueanfrage.php, den beiden neueanfrage-old Sicherungen und zeiterfassung/api/vacations.php fest eingeschaltet. Die oeffentliche Impfwarteliste zeigte Patienten bei einem Fatal Error sogar Meldung, Dateipfad und Zeilennummer. Ueberall auf log_errors umgestellt: E_ALL wird weiterhin vollstaendig erfasst, landet aber im Server-Log statt auf der Seite. Der Shutdown-Handler der Impfwarteliste protokolliert die Details und zeigt nur noch einen neutralen Hinweis. Auch register.php gab die rohe Exception-Meldung aus. 2. SendMailMessageSilent() verschluckte jeden Fehler Der catch-Block war leer. Ein SMTP-Ausfall war dadurch von aussen nicht von einem falschen Code zu unterscheiden - der Benutzer landete auf verify_2fa.php und wartete auf eine Mail, die nie kam. Die Funktion protokolliert jetzt und liefert einen bool zurueck. login.php wertet das aus, nimmt bei Fehlschlag den 2FA-Datensatz und die Session-Vormerkung zurueck und sagt es auf der Login-Seite, statt weiterzuleiten. Nebenbei entfernt: ein uebrig gebliebenes echo, das den Mailserver-Namen mitten in die Seite schrieb, sowie ein zweites mysqli_fetch_assoc() auf demselben Result, das nur NULL liefern konnte. 3. register.php verschickte keine Bestaetigungsmail mailreg blieb 0, jeder neue Benutzer landete nach dem Login auf der Aufforderung, die Authentifizierung selbst anzustossen. Die Mail geht jetzt direkt nach der Registrierung raus. Erzeugung und Text liegen in der neuen Funktion sendeAuthentifizierungsMail(), die authmeldung.php ebenfalls benutzt - dort wurde der Rueckgabewert des Versands bisher einer Variablen zugewiesen und nie ausgewertet, die Seite meldete Erfolg auch bei fehlgeschlagenem Versand. Der Versand laeuft nach dem commit(), deshalb prueft der catch-Block in register.php jetzt inTransaction(), bevor er rollBack() aufruft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
418 lines
12 KiB
PHP
418 lines
12 KiB
PHP
<?php
|
|
session_start();
|
|
require_once(__DIR__ . '/../inc/config.inc.php');
|
|
require_once(__DIR__ . '/../inc/functions.inc.php');
|
|
require_once(__DIR__ . '/../inc/vacation_absence.inc.php');
|
|
|
|
// Fehler protokollieren, aber nicht an Besucher ausgeben.
|
|
ini_set('display_errors', '0');
|
|
ini_set('log_errors', '1');
|
|
ini_set('display_startup_errors', '0');
|
|
error_reporting(E_ALL);
|
|
|
|
if (!function_exists('vacationApiTableHasColumn')) {
|
|
function vacationApiTableHasColumn(PDO $pdo, string $table, string $column): bool
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
"SELECT COUNT(*)
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = :table_name
|
|
AND COLUMN_NAME = :column_name"
|
|
);
|
|
$stmt->execute([
|
|
'table_name' => $table,
|
|
'column_name' => $column,
|
|
]);
|
|
return (int)$stmt->fetchColumn() > 0;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiLower')) {
|
|
function vacationApiLower(?string $value): string
|
|
{
|
|
$value = trim((string)$value);
|
|
if ($value === '') {
|
|
return '';
|
|
}
|
|
|
|
if (function_exists('mb_strtolower')) {
|
|
return mb_strtolower($value, 'UTF-8');
|
|
}
|
|
|
|
return strtolower($value);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiNormalizeAbsenceType')) {
|
|
function vacationApiNormalizeAbsenceType(?string $type): string
|
|
{
|
|
$type = vacationApiLower($type);
|
|
if ($type === '') {
|
|
return vacationAbsenceDefaultReason();
|
|
}
|
|
|
|
$type = strtr($type, [
|
|
'ä' => 'ae',
|
|
'ö' => 'oe',
|
|
'ü' => 'ue',
|
|
'ß' => 'ss',
|
|
]);
|
|
|
|
$type = preg_replace('/[^a-z0-9]+/u', '_', $type);
|
|
$type = trim((string)$type, '_');
|
|
|
|
$map = [
|
|
'urlaub' => 'urlaub',
|
|
'urlaubsantrag' => 'urlaub',
|
|
'krankheit_mit_attest' => 'krankheit_mit_atest',
|
|
'krankheit_mit_atest' => 'krankheit_mit_atest',
|
|
'krank_mit_attest' => 'krankheit_mit_atest',
|
|
'krankheit_ohne_attest' => 'krankheit_ohne_atest',
|
|
'krankheit_ohne_atest' => 'krankheit_ohne_atest',
|
|
'krank_ohne_attest' => 'krankheit_ohne_atest',
|
|
'berufsschule' => 'berufsschule',
|
|
'weiterbildung' => 'weiterbildung',
|
|
'persoenliche_gruende' => 'persoenliche_gruende',
|
|
'persoenliche_gruende_' => 'persoenliche_gruende',
|
|
'persoenliche_gruende_mit' => 'persoenliche_gruende',
|
|
'sonstiges' => 'sonstiges',
|
|
];
|
|
|
|
return vacationAbsenceNormalizeReason($map[$type] ?? $type);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiNormalizeStatus')) {
|
|
function vacationApiNormalizeStatus(?string $status): string
|
|
{
|
|
$status = vacationApiLower($status);
|
|
if ($status === '') {
|
|
return 'beantragt';
|
|
}
|
|
|
|
$status = preg_replace('/\s+/u', ' ', $status);
|
|
return $status;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiTypeLabel')) {
|
|
function vacationApiTypeLabel(string $type): string
|
|
{
|
|
return vacationAbsenceReasonLabel($type);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiStatusLabel')) {
|
|
function vacationApiStatusLabel(string $status): string
|
|
{
|
|
$labels = [
|
|
'genehmigt' => 'Genehmigt',
|
|
'beantragt' => 'Beantragt',
|
|
'abgelehnt' => 'Abgelehnt',
|
|
];
|
|
|
|
return $labels[$status] ?? ucfirst($status);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiStatusColor')) {
|
|
function vacationApiStatusColor(string $status): string
|
|
{
|
|
$colors = [
|
|
'genehmigt' => '#28a745',
|
|
'beantragt' => '#f0ad4e',
|
|
'abgelehnt' => '#6c757d',
|
|
];
|
|
|
|
return $colors[$status] ?? '#17a2b8';
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiTypeColor')) {
|
|
function vacationApiTypeColor(string $type): string
|
|
{
|
|
$colors = [
|
|
'urlaub' => '#2f9e44',
|
|
'krankheit_mit_atest' => '#d9480f',
|
|
'krankheit_ohne_atest' => '#c92a2a',
|
|
'berufsschule' => '#1971c2',
|
|
'weiterbildung' => '#5f3dc4',
|
|
'persoenliche_gruende' => '#e67700',
|
|
'sonstiges' => '#495057',
|
|
];
|
|
|
|
return $colors[$type] ?? '#0d6efd';
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiRequestedTypes')) {
|
|
function vacationApiRequestedTypes(?string $rawTypes, string $scope): ?array
|
|
{
|
|
$rawTypes = trim((string)$rawTypes);
|
|
if ($rawTypes === '') {
|
|
if ($scope === 'team') {
|
|
return ['urlaub'];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
$normalized = vacationApiLower($rawTypes);
|
|
if (in_array($normalized, ['all', '*'], true)) {
|
|
return null;
|
|
}
|
|
|
|
$types = [];
|
|
foreach (preg_split('/\s*,\s*/', $rawTypes) as $rawType) {
|
|
$normalizedType = vacationApiNormalizeAbsenceType($rawType);
|
|
if ($normalizedType !== '') {
|
|
$types[$normalizedType] = true;
|
|
}
|
|
}
|
|
|
|
$types = array_keys($types);
|
|
sort($types);
|
|
|
|
return $types;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('vacationApiStatusAllowed')) {
|
|
function vacationApiStatusAllowed(string $status, string $mode): bool
|
|
{
|
|
$mode = vacationApiLower($mode);
|
|
$status = vacationApiNormalizeStatus($status);
|
|
|
|
switch ($mode) {
|
|
case 'approved':
|
|
return $status === 'genehmigt';
|
|
case 'open':
|
|
return $status === 'beantragt' || $status === '';
|
|
case 'active':
|
|
return $status === 'genehmigt' || $status === 'beantragt' || $status === '';
|
|
case 'rejected':
|
|
return $status === 'abgelehnt';
|
|
case 'all':
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$user = check_user();
|
|
$isAdmin = is_admin_user();
|
|
|
|
$start = trim((string)($_GET['start'] ?? ''));
|
|
$end = trim((string)($_GET['end'] ?? ''));
|
|
|
|
if ($start === '' || $end === '') {
|
|
http_response_code(400);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode(['error' => 'start and end required']);
|
|
exit;
|
|
}
|
|
|
|
$onlyApproved = isset($_GET['only_approved']) && ($_GET['only_approved'] == '1' || $_GET['only_approved'] === 'true');
|
|
$public = isset($_GET['public']) && ($_GET['public'] == '1' || $_GET['public'] === 'true');
|
|
$includeRejected = isset($_GET['include_rejected']) && ($_GET['include_rejected'] == '1' || $_GET['include_rejected'] === 'true');
|
|
$onlyPersonal = isset($_GET['only_personal']) && ($_GET['only_personal'] == '1' || $_GET['only_personal'] === 'true');
|
|
$publicAll = isset($_GET['public_all']) && ($_GET['public_all'] == '1' || $_GET['public_all'] === 'true');
|
|
|
|
$scope = strtolower(trim((string)($_GET['scope'] ?? '')));
|
|
if ($scope === '') {
|
|
if ($onlyPersonal) {
|
|
$scope = 'personal';
|
|
} elseif ($public || $publicAll) {
|
|
$scope = 'team';
|
|
} elseif ($isAdmin) {
|
|
$scope = 'team';
|
|
} else {
|
|
$scope = 'personal';
|
|
}
|
|
}
|
|
|
|
if (!in_array($scope, ['personal', 'team', 'admin_all'], true)) {
|
|
$scope = 'personal';
|
|
}
|
|
|
|
if ($scope === 'admin_all' && !$isAdmin) {
|
|
$scope = 'personal';
|
|
}
|
|
|
|
$statusMode = strtolower(trim((string)($_GET['status_filter'] ?? '')));
|
|
if ($statusMode === '') {
|
|
if ($onlyApproved) {
|
|
$statusMode = 'approved';
|
|
} elseif ($includeRejected) {
|
|
$statusMode = 'all';
|
|
} elseif ($scope === 'personal' || $scope === 'admin_all') {
|
|
$statusMode = 'all';
|
|
} else {
|
|
$statusMode = 'active';
|
|
}
|
|
}
|
|
|
|
$includeCompany = !isset($_GET['include_company']) || $_GET['include_company'] === '1' || $_GET['include_company'] === 'true';
|
|
$requestedTypes = vacationApiRequestedTypes($_GET['absence_types'] ?? null, $scope);
|
|
|
|
$hasAbsenceReasonColumn = vacationApiTableHasColumn($pdo, 'vacations', 'absence_reason');
|
|
$absenceTypeSelect = $hasAbsenceReasonColumn ? 'v.absence_reason' : "'" . vacationAbsenceDefaultReason() . "'";
|
|
|
|
$selectColumns = "
|
|
SELECT
|
|
v.id,
|
|
v.user_id,
|
|
v.start_date,
|
|
v.end_date,
|
|
v.days,
|
|
v.status,
|
|
v.comment_user,
|
|
u.vorname,
|
|
u.nachname,
|
|
{$absenceTypeSelect} AS absence_type
|
|
FROM vacations v
|
|
JOIN users u ON v.user_id = u.id
|
|
WHERE v.start_date <= ?
|
|
AND v.end_date >= ?
|
|
";
|
|
|
|
$params = [$end, $start];
|
|
|
|
if ($scope === 'personal') {
|
|
$selectColumns .= " AND v.user_id = ?";
|
|
$params[] = $_SESSION['userid'];
|
|
} elseif ($scope === 'team' || $scope === 'admin_all') {
|
|
// No further restriction here. Scope filters are applied in PHP so
|
|
// the query remains flexible for future admin and team views.
|
|
}
|
|
|
|
$selectColumns .= " ORDER BY v.start_date ASC, v.id ASC";
|
|
|
|
$stmt = $pdo->prepare($selectColumns);
|
|
$stmt->execute($params);
|
|
$vacations = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$events = [];
|
|
|
|
foreach ($vacations as $v) {
|
|
$absenceType = vacationApiNormalizeAbsenceType($v['absence_type'] ?? '');
|
|
$status = vacationApiNormalizeStatus($v['status'] ?? '');
|
|
|
|
if ($scope === 'team' && $absenceType !== 'urlaub') {
|
|
continue;
|
|
}
|
|
|
|
if ($requestedTypes !== null && !in_array($absenceType, $requestedTypes, true)) {
|
|
continue;
|
|
}
|
|
|
|
if (!vacationApiStatusAllowed($status, $statusMode)) {
|
|
continue;
|
|
}
|
|
|
|
$employeeName = trim((string)($v['vorname'] ?? '') . ' ' . (string)($v['nachname'] ?? ''));
|
|
$typeLabel = vacationApiTypeLabel($absenceType);
|
|
$statusLabel = vacationApiStatusLabel($status);
|
|
|
|
if ($scope === 'personal') {
|
|
$title = $typeLabel;
|
|
} elseif ($employeeName !== '') {
|
|
$title = $employeeName . ' - ' . $typeLabel;
|
|
} else {
|
|
$title = $typeLabel;
|
|
}
|
|
|
|
if ($status !== '' && $status !== 'genehmigt') {
|
|
$title .= ' (' . $statusLabel . ')';
|
|
}
|
|
|
|
try {
|
|
$endInclusive = (new DateTime($v['end_date']))->modify('+1 day')->format('Y-m-d');
|
|
} catch (Exception $e) {
|
|
$endInclusive = $v['start_date'];
|
|
}
|
|
|
|
$backgroundColor = vacationApiTypeColor($absenceType);
|
|
if ($status === 'abgelehnt') {
|
|
$backgroundColor = vacationApiStatusColor($status);
|
|
}
|
|
|
|
$events[] = [
|
|
'id' => 'vac_' . $v['id'],
|
|
'title' => $title,
|
|
'start' => $v['start_date'],
|
|
'end' => $endInclusive,
|
|
'allDay' => true,
|
|
'backgroundColor' => $backgroundColor,
|
|
'borderColor' => $backgroundColor,
|
|
'textColor' => '#ffffff',
|
|
'extendedProps' => [
|
|
'type' => 'user',
|
|
'user_id' => $v['user_id'],
|
|
'employee_name' => $employeeName,
|
|
'absence_type' => $absenceType,
|
|
'absence_label' => $typeLabel,
|
|
'status' => $v['status'],
|
|
'status_label' => $statusLabel,
|
|
'comment' => $v['comment_user'] ?? '',
|
|
'scope' => $scope,
|
|
],
|
|
];
|
|
}
|
|
|
|
if ($includeCompany) {
|
|
$stmt = $pdo->prepare("SELECT id, start_date, end_date, description, vertretung, vertretertelefon, vertreteradresse, vertreterurl FROM company_holidays WHERE start_date <= ? AND end_date >= ? ORDER BY start_date");
|
|
$stmt->execute([$end, $start]);
|
|
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
foreach ($holidays as $h) {
|
|
try {
|
|
$endInclusive = (new DateTime($h['end_date']))->modify('+1 day')->format('Y-m-d');
|
|
} catch (Exception $e) {
|
|
$endInclusive = $h['start_date'];
|
|
}
|
|
|
|
$description = trim((string)($h['description'] ?? ''));
|
|
$title = $description !== '' ? $description : 'Betriebsurlaub';
|
|
|
|
$events[] = [
|
|
'id' => 'com_' . $h['id'],
|
|
'title' => $title,
|
|
'start' => $h['start_date'],
|
|
'end' => $endInclusive,
|
|
'allDay' => true,
|
|
'backgroundColor' => '#0b7285',
|
|
'borderColor' => '#0b7285',
|
|
'textColor' => '#ffffff',
|
|
'extendedProps' => [
|
|
'type' => 'company',
|
|
'description' => $description,
|
|
'vertretung' => $h['vertretung'] ?? '',
|
|
'vertretertelefon' => $h['vertretertelefon'] ?? '',
|
|
'vertreteradresse' => $h['vertreteradresse'] ?? '',
|
|
'vertreterurl' => $h['vertreterurl'] ?? '',
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$json = json_encode(
|
|
$events,
|
|
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE
|
|
);
|
|
|
|
if ($json === false) {
|
|
http_response_code(500);
|
|
echo json_encode([
|
|
'error' => 'Kalenderdaten konnten nicht kodiert werden.',
|
|
'json_error' => json_last_error_msg(),
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
}
|
|
|
|
echo $json;
|
|
?>
|