[]], 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 event_path(array $event): string { if (!empty($event['slug'])) { return '/event.php?slug=' . rawurlencode($event['slug']); } if (!empty($event['id'])) { return '/event.php?id=' . rawurlencode($event['id']); } return '/event.php'; } function event_url(array $event): string { return rtrim(BASE_URL, '/') . event_path($event); } 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'], "url" => event_url($e), "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 = '
'; $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( '/]*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 ''; }, $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; }