Files
Walschleber-Kultour-CMS/functions.php
T
2026-01-11 15:40:15 +00:00

218 lines
7.6 KiB
PHP

<?php
// functions.php
declare(strict_types=1);
require_once __DIR__ . '/config.php';
function ensure_storage(): void {
if (!is_dir(dirname(DATA_FILE))) {
@mkdir(dirname(DATA_FILE), 0775, true);
}
if (!is_file(DATA_FILE)) {
file_put_contents(DATA_FILE, json_encode(['events' => []], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
if (!is_dir(UPLOAD_DIR)) {
@mkdir(UPLOAD_DIR, 0775, true);
}
}
function load_events(): array {
ensure_storage();
$raw = file_get_contents(DATA_FILE);
$data = json_decode($raw ?: '{"events":[]}', true);
return is_array($data) && isset($data['events']) ? $data['events'] : [];
}
function save_events(array $events): void {
ensure_storage();
$fp = fopen(DATA_FILE, 'c+');
if ($fp === false) {
throw new RuntimeException('Kann Datei nicht öffnen: ' . DATA_FILE);
}
flock($fp, LOCK_EX);
ftruncate($fp, 0);
fwrite($fp, json_encode(['events' => array_values($events)], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
}
function by_datetime_asc(array $a, array $b): int {
return strtotime($a['date'].' '.$a['time']) <=> strtotime($b['date'].' '.$b['time']);
}
function by_datetime_desc(array $a, array $b): int {
return strtotime($b['date'].' '.$b['time']) <=> strtotime($a['date'].' '.$a['time']);
}
function now_iso(): string {
return date('Y-m-d\TH:i:sP');
}
function make_slug(string $title, string $date): string {
$t = mb_strtolower($title, 'UTF-8');
$t = preg_replace('~[^\pL\d]+~u', '-', $t);
$t = trim($t, '-');
$t = preg_replace('~[^-\w]+~', '', $t);
$t = preg_replace('~-+~', '-', $t);
return $t . '-' . $date;
}
function sanitize_text(string $s): string {
return trim($s);
}
function is_upcoming(array $event): bool {
$ts = strtotime($event['date'].' '.$event['time']);
return $ts >= time();
}
function handle_image_upload(?array $file): ?string {
if (!$file || ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
return null;
}
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload-Fehler (Code '.$file['error'].')');
}
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!isset($allowed[$mime])) {
throw new RuntimeException('Nur JPG/PNG/WEBP erlaubt.');
}
$ext = $allowed[$mime];
$basename = 'event_'.date('Ymd_His').'_' . bin2hex(random_bytes(4)) . '.' . $ext;
$dest = rtrim(UPLOAD_DIR, '/').'/'.$basename;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
throw new RuntimeException('Konnte Datei nicht speichern.');
}
return '/uploads/'.$basename; // Pfad relativ zur Webroot
}
function render_event_jsonld_graph(array $events): string {
$graph = [];
foreach ($events as $e) {
$start = $e['date'] . 'T' . ($e['time'] ?? '19:30') . ':00+02:00'; // Berlin Zeit im Sommer; vereinfacht
$price = $e['price'] ?? '';
$offers = $price !== '' ? [
"@type" => "Offer",
"price" => preg_replace('/[^\d,\.]/', '', $price),
"priceCurrency" => "EUR",
"availability" => "https://schema.org/InStock"
] : null;
$graph[] = array_filter([
"@type" => "Event",
"name" => $e['title'],
"startDate" => $start,
"eventStatus" => "https://schema.org/EventScheduled",
"eventAttendanceMode" => "https://schema.org/OfflineEventAttendanceMode",
"location" => [
"@type" => "Place",
"name" => $e['venue_name'] ?? "Veranstaltungsort",
"address" => [
"@type" => "PostalAddress",
"streetAddress" => $e['venue_address'] ?? "",
"addressLocality" => "Walschleben",
"postalCode" => "99189",
"addressCountry" => "DE"
],
],
"image" => isset($e['image']) && $e['image'] !== '' ? [ BASE_URL . $e['image'] ] : [ BASE_URL . DEFAULT_OG_IMAGE ],
"description" => $e['description'] ?? "",
"organizer" => [
"@type" => "Organization",
"name" => SITE_NAME,
"url" => BASE_URL
],
"offers" => $offers
]);
}
return json_encode([
"@context" => "https://schema.org",
"@graph" => $graph
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
function h(?string $s): string {
return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
/**
* Erkennt, ob HTML-Tags enthalten sind.
*/
function has_html_tags(string $s): bool {
return (bool) preg_match('/<\s*\w+[^>]*>/', $s);
}
/**
* Erlaubte, sichere HTML-Ausgabe (Whitelist + Attribut-Filter).
* Erlaubt: p, br, ul/ol/li, b/strong, i/em, u, a, blockquote, h3-h5
* Entfernt: on* Handler, javascript: URLs, style/iframe/script usw.
* Fügt bei Links rel="noopener nofollow ugc" hinzu und erlaubt nur http(s).
*/
function render_rich_text(string $input): string {
$input = trim($input);
// Fall A: Kein HTML -> wir schützen & konvertieren Zeilenumbrüche
if (!has_html_tags($input)) {
return nl2br(h($input));
}
// Fall B: HTML vorhanden -> nur erlaubte Tags behalten
$allowed = '<p><br><ul><ol><li><b><strong><i><em><u><a><blockquote><h3><h4><h5>';
$html = strip_tags($input, $allowed);
// Gefährliche Event-Handler entfernen (on*)
$html = preg_replace('/\s+on\w+\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $html);
// style-Attribute entfernen (vermeidet versteckte JS/CSS-Tricks)
$html = preg_replace('/\s+style\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $html);
// href bereinigen: nur http/https erlauben, sonst entfernen
// Außerdem rel/target ergänzen
$html = preg_replace_callback(
'/<a\s+([^>]*href\s*=\s*(["\'])(.*?)\2[^>]*)>/i',
function ($m) {
$attr = $m[1];
$url = $m[3];
// javascript:, data:, mailto: usw. blocken (nur http/https)
if (!preg_match('~^https?://~i', $url)) {
// href wegnehmen
$attr = preg_replace('/\s*href\s*=\s*(".*?"|\'.*?\'|[^\s>]+)/i', '', $attr);
$url = null;
}
// target & rel setzen
if ($url) {
// target="_blank"
if (!preg_match('/\btarget\s*=/i', $attr)) {
$attr .= ' target="_blank"';
}
// rel ergänzen
if (preg_match('/\brel\s*=\s*("|\')(.*?)\1/i', $attr, $r)) {
$rels = array_map('trim', explode(' ', $r[2] ?: ''));
foreach (['noopener','nofollow','ugc'] as $need) {
if (!in_array($need, $rels, true)) $rels[] = $need;
}
$attr = preg_replace('/\brel\s*=\s*("|\')(.*?)\1/i', ' rel="'.implode(' ', $rels).'"', $attr);
} else {
$attr .= ' rel="noopener nofollow ugc"';
}
}
return '<a ' . trim($attr) . '>';
},
$html
);
// Restliche "href='javascript:…'"-Muster sicherheitshalber neutralisieren
$html = preg_replace('/href\s*=\s*(["\'])\s*javascript:[^"\']*\1/i', '', $html);
// Doppelte/überflüssige Leerzeichen in Tags aufräumen
$html = preg_replace('/\s{2,}/', ' ', $html);
return $html;
}