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
@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
const BRANDSCHUTZ_VALIDITY_DAYS = 365;
function brandschutz_csrf_token(): string
{
if (empty($_SESSION['brandschutz_csrf'])) {
$_SESSION['brandschutz_csrf'] = bin2hex(random_bytes(32));
}
return (string) $_SESSION['brandschutz_csrf'];
}
function brandschutz_verify_csrf($postedToken): void
{
$sessionToken = $_SESSION['brandschutz_csrf'] ?? '';
if (!is_string($postedToken) || $sessionToken === '' || !hash_equals((string) $sessionToken, $postedToken)) {
throw new InvalidArgumentException('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
}
}
function brandschutz_is_valid_date(string $value): bool
{
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
$errors = DateTimeImmutable::getLastErrors();
return $date !== false
&& ($errors === false || ($errors['warning_count'] === 0 && $errors['error_count'] === 0))
&& $date->format('Y-m-d') === $value;
}
function brandschutz_today(?DateTimeImmutable $today = null): DateTimeImmutable
{
if ($today !== null) {
return $today->setTime(0, 0);
}
return new DateTimeImmutable('today', new DateTimeZone('Europe/Berlin'));
}
/**
* Berechnet den fachlichen Schulungsstatus an einer Stelle für Intranet,
* Adminpanel und Erinnerungsjob.
*/
function brandschutz_training_status(?string $lastTraining, ?DateTimeImmutable $today = null): array
{
$today = brandschutz_today($today);
$lastTraining = trim((string) $lastTraining);
if ($lastTraining === '' || !brandschutz_is_valid_date($lastTraining)) {
return [
'last_training' => null,
'last_training_display' => 'Nie',
'due_date' => null,
'due_date_display' => 'Sofort fällig',
'days_until_due' => -1,
'days_overdue' => 1,
'is_due' => true,
'status_key' => 'never',
'status_text' => 'Nie geschult',
'status_class' => 'bs-status-overdue',
'overdue_text' => 'Keine Schulung hinterlegt – sofort fällig',
'overdue_class' => 'overdue',
];
}
$trainingDate = new DateTimeImmutable($lastTraining, new DateTimeZone('Europe/Berlin'));
$dueDate = $trainingDate->modify('+' . BRANDSCHUTZ_VALIDITY_DAYS . ' days');
// Bestehende 24h-Tagesberechnung bewusst beibehalten (inkl. DST-Verhalten).
$daysUntilDue = (int) (($dueDate->getTimestamp() - $today->getTimestamp()) / 86400);
$daysOverdue = $daysUntilDue <= 0 ? abs($daysUntilDue) : 0;
if ($daysUntilDue > 30) {
$statusKey = 'current';
$statusText = 'Aktuell';
$statusClass = 'bs-status-ok';
} elseif ($daysUntilDue > 0) {
$statusKey = 'warning';
$statusText = 'Bald fällig';
$statusClass = 'bs-status-warning';
} elseif ($daysUntilDue > -120) {
$statusKey = 'overdue';
$statusText = $daysOverdue . ' Tage überfällig';
$statusClass = 'bs-status-overdue';
} else {
$statusKey = 'critical';
$statusText = $daysOverdue . ' Tage überfällig';
$statusClass = 'bs-status-critical';
}
return [
'last_training' => $lastTraining,
'last_training_display' => $trainingDate->format('d.m.Y'),
'due_date' => $dueDate->format('Y-m-d'),
'due_date_display' => $dueDate->format('d.m.Y'),
'days_until_due' => $daysUntilDue,
'days_overdue' => $daysOverdue,
'is_due' => $daysUntilDue <= 0,
'status_key' => $statusKey,
'status_text' => $statusText,
'status_class' => $statusClass,
'overdue_text' => $daysUntilDue > 0
? 'Nicht überfällig (' . $daysUntilDue . ' Tage verbleibend)'
: $daysOverdue . ' Tage überfällig',
'overdue_class' => $daysUntilDue > 0 ? 'ok' : 'overdue',
];
}
function brandschutz_reminder_actions(array $status, int $reminderCount): array
{
$daysUntilDue = (int) ($status['days_until_due'] ?? 0);
$daysOverdue = (int) ($status['days_overdue'] ?? 0);
return [
'pre_due' => $daysUntilDue >= 0 && $daysUntilDue <= 30 && $reminderCount === 0,
'overdue' => $daysOverdue > 0,
'escalate' => $daysOverdue >= 120,
];
}
function brandschutz_display_name(array $user, string $fallback = 'Unbekannt'): string
{
$fullName = trim((string) ($user['first_name'] ?? '') . ' ' . (string) ($user['last_name'] ?? ''));
if ($fullName !== '') {
return $fullName;
}
$mail = trim((string) ($user['mail'] ?? ''));
return $mail !== '' ? $mail : $fallback;
}
function brandschutz_greeting(array $user): string
{
$firstName = trim((string) ($user['first_name'] ?? ''));
return $firstName !== '' ? 'Hallo ' . $firstName : 'Hallo';
}
function brandschutz_training_url(?string $host = null): string
{
$host = trim((string) ($host ?? ($_SERVER['HTTP_HOST'] ?? '')));
$host = $host !== '' ? $host : 'localhost';
return 'https://' . $host . '/intranet_repo/projects/brandschutz/brandschutz.php';
}
function brandschutz_store_training_date(PDO $pdo, int $userId, string $date, bool $resetCounters): void
{
if ($userId <= 0 || !brandschutz_is_valid_date($date)) {
throw new InvalidArgumentException('Ungültiges Schulungsdatum.');
}
if ($resetCounters) {
$sql = 'INSERT INTO brandschutz_training '
. '(user_id, last_training_date, reminder_count, admin_reminder_count) '
. 'VALUES (?, ?, 0, 0) '
. 'ON CONFLICT (user_id) DO UPDATE SET '
. 'last_training_date = EXCLUDED.last_training_date, reminder_count = 0, admin_reminder_count = 0';
} else {
$sql = 'INSERT INTO brandschutz_training '
. '(user_id, last_training_date, reminder_count, admin_reminder_count) '
. 'VALUES (?, ?, 0, 0) '
. 'ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date';
}
$statement = $pdo->prepare($sql);
$statement->execute([$userId, $date]);
}
function brandschutz_log_admin_message(
PDO $pdo,
string $senderName,
string $senderEmail,
string $subject,
string $message,
string $status = 'done',
?array $data = null
): void {
if (!in_array($status, ['done', 'pending'], true)) {
throw new InvalidArgumentException('Ungültiger Nachrichtenstatus.');
}
$dataJson = $data === null ? null : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
if ($status === 'done') {
$statement = $pdo->prepare(
"INSERT INTO admin_messages "
. "(type, sender_name, sender_email, subject, message, status, processed_at, created_at, data_json) "
. "VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW(), ?)"
);
} else {
$statement = $pdo->prepare(
"INSERT INTO admin_messages "
. "(type, sender_name, sender_email, subject, message, status, processed_at, created_at, data_json) "
. "VALUES ('system', ?, ?, ?, ?, 'pending', NULL, NOW(), ?)"
);
}
$statement->execute([$senderName, $senderEmail, $subject, $message, $dataJson]);
}