[]], 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 = '